From 69ca92f0b2e077335ca34dd9e15001a86511cf58 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Wed, 24 Feb 2021 13:49:34 -0700 Subject: [PATCH 01/46] chore: remove old reset-vscode script --- ci/dev/reset-vscode.sh | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100755 ci/dev/reset-vscode.sh diff --git a/ci/dev/reset-vscode.sh b/ci/dev/reset-vscode.sh deleted file mode 100755 index 889ef369a..000000000 --- a/ci/dev/reset-vscode.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -main() { - cd "$(dirname "$0")/../.." - - cd ./lib/vscode - git add -A - git reset --hard -} - -main "$@" From 977c579c02a5245b307544a49608db8989959de5 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Wed, 24 Feb 2021 13:49:48 -0700 Subject: [PATCH 02/46] feat: add update-vscode.sh script --- ci/dev/update-vscode.sh | 59 +++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100755 ci/dev/update-vscode.sh diff --git a/ci/dev/update-vscode.sh b/ci/dev/update-vscode.sh new file mode 100755 index 000000000..b05fb7abd --- /dev/null +++ b/ci/dev/update-vscode.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +main() { + cd "$(dirname "$0")/../.." + + # Check if the remote exists + # if it doesn't, we add it + if ! git config remote.vscode.url > /dev/null; then + echo "Could not find 'vscode' as a remote" + echo "Adding with: git remote add -f vscode https://github.com/microsoft/vscode.git &> /dev/null" + echo "Supressing output with '&> /dev/null'" + git remote add -f vscode https://github.com/microsoft/vscode.git &> /dev/null + fi + + # Ask which version we should update to + # In the future, we'll automate this and grab the latest version automatically + read -p "What version of VSCode would you like to update to? (i.e. 1.52) " VSCODE_VERSION_TO_UPDATE + + # Check that this version exists + if [[ -z $(git ls-remote --heads vscode release/$VSCODE_VERSION_TO_UPDATE) ]]; then + echo "Oops, that doesn't look like a valid version." + echo "You entered: $VSCODE_VERSION_TO_UPDATE" + echo "Verify that this branches exists here: https://github.com/microsoft/vscode/branches/all?query=release%2F$VSCODE_VERSION_TO_UPDATE" + exit 1 + fi + + echo -e "Great! We'll prep a PR for updating to $VSCODE_VERSION_TO_UPDATE\n" + + # Check if GitHub CLI is installed + if ! command -v gh &> /dev/null; then + echo "GitHub CLI could not be found." + echo "If you install it before you run this script next time, we'll open a draft PR for you!" + echo -e "See docs here: https://github.com/cli/cli#installation\n" + exit + fi + + # Push branch to remote if not already pushed + # If we don't do this, the opening a draft PR step won't work + # because it will stop and ask where you want to push the branch + CURRENT_BRANCH=$(git branch --show-current) + if [[ -z $(git ls-remote --heads origin $CURRENT_BRANCH) ]]; then + echo "Doesn't look like you've pushed this branch to remote" + echo -e "Pushing now using: git push origin $CURRENT_BRANCH\n" + git push origin $CURRENT_BRANCH + fi + + echo "Opening a draft PR on GitHub" + # To read about these flags, visit the docs: https://cli.github.com/manual/gh_pr_create + gh pr create --base master --title "feat(vscode): update to version $VSCODE_VERSION_TO_UPDATE" --body "This PR updates `/lib/vscode` to version: $VSCODE_VERSION_TO_UPDATE" --reviewer @cdr/code-server-reviewers --repo cdr/code-server --draft + + + echo "Going to try to update vscode for you..." + echo -e "Running: git subtree pull --prefix lib/vscode vscode release/${VSCODE_VERSION_TO_UPDATE} --squash\n" + # Try to run subtree update command + git subtree pull --prefix lib/vscode vscode release/${VSCODE_VERSION_TO_UPDATE} --squash --message "chore(vscode): update to $VSCODE_VERSION_TO_UPDATE" +} + +main "$@" diff --git a/package.json b/package.json index decec942a..a41bd6b9d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "repository": "https://github.com/cdr/code-server", "scripts": { "clean": "./ci/build/clean.sh", - "vscode:reset": "./ci/dev/reset-vscode.sh", "build": "./ci/build/build-code-server.sh", "build:vscode": "./ci/build/build-vscode.sh", "release": "./ci/build/build-release.sh", @@ -20,6 +19,7 @@ "test:standalone-release": "./ci/build/test-standalone-release.sh", "package": "./ci/build/build-packages.sh", "postinstall": "./ci/dev/postinstall.sh", + "update:vscode": "./ci/dev/update-vscode.sh", "_____": "", "fmt": "./ci/dev/fmt.sh", "lint": "./ci/dev/lint.sh", From db3a13ba06933c0154180066a5239b3cf4ef644f Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Thu, 25 Feb 2021 11:20:25 -0700 Subject: [PATCH 03/46] chore: fix script --- ci/dev/update-vscode.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/dev/update-vscode.sh b/ci/dev/update-vscode.sh index b05fb7abd..2c05431ad 100755 --- a/ci/dev/update-vscode.sh +++ b/ci/dev/update-vscode.sh @@ -47,7 +47,7 @@ main() { echo "Opening a draft PR on GitHub" # To read about these flags, visit the docs: https://cli.github.com/manual/gh_pr_create - gh pr create --base master --title "feat(vscode): update to version $VSCODE_VERSION_TO_UPDATE" --body "This PR updates `/lib/vscode` to version: $VSCODE_VERSION_TO_UPDATE" --reviewer @cdr/code-server-reviewers --repo cdr/code-server --draft + gh pr create --base master --title "feat(vscode): update to version $VSCODE_VERSION_TO_UPDATE" --body "This PR updates vscode to version: $VSCODE_VERSION_TO_UPDATE" --reviewer @cdr/code-server-reviewers --repo cdr/code-server --draft echo "Going to try to update vscode for you..." From 89b6e0164fa770333755b11504e19a4232b1a2d4 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Thu, 25 Feb 2021 11:26:29 -0700 Subject: [PATCH 04/46] Squashed 'lib/vscode/' changes from 3e344b17b7b..622cb03f7e0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 622cb03f7e0 Merge pull request #116444 from microsoft/alex/stable-fix-116060 5933e313e5d Fixes #116060: Clone minimap options before changing them f3a12e053e1 Pick up TS 4.1.5 (#116380) 3b9cef2b8d1 Bump Emmet (#116230) 8757f67bcda fix #116049 (#116319) 73c0a56bbd2 Merge pull request #116311 from microsoft/joh/fix/116094 5cf34afa107 Use weak shell quoting for npm tasks with -- (#116309) 6635ca9a64e Merge pull request #116245 from microsoft/connor4312/debug-repl-fix 65406fcea58 fix https://github.com/microsoft/vscode/issues/116094 43e11faf4ee fix: CreateFile ERROR_FILE_NOT_FOUND from crashpad handler (#116256) 17d65083f6c debug: replace element when appending text to ensure updates in repl 9d194eac0d7 This reverts us to the previous distro commit (#116218) 1fe57f42761 Merge pull request #115789 from microsoft/rebornix/fix-notebook-runstate d63ab6acdbd bump version to 1.53.2 (#116228) 4aff7304539 Merge pull request #115772 from microsoft/misolori/1.53/create-pr-icon 678843ff3ad fixes #115810 (#115943) e1ec11f5183 always fallback to plain text (#115860) (#116177) 615ea78d96a status - update background color (#115886) (#116181) 00d7f953055 add changes (#116223) 3c19fc731cb Pick up the official TS 4.1.4 build (#116222) 5d424b828ad Pick up new distro version and bump version (#116149) ee2c947e064 ci: update newer 11.2.1 for CVE-2021-21148 (#115951) e23884b9476 re #115717 5da053f081f Update Codicons: add 'git-pull-request-create' https://github.com/microsoft/vscode-codicons/commit/686357b7174e7b3113640fea20db7efc84d8d1d4 7f04ecd68be use PersistKeySet (#115744) 8490d3dde47 Merge pull request #115701 from microsoft/sandy081/fix115699 1d6c8826375 Fix #115699 f84decb78f3 Merge pull request #115686 from microsoft/isidorn/openEditorsCollapsed 203b86d14c5 fixes 115685 83f7a99bd95 Azure DevOps - Add global variable for VSCODE_QUALITY (#115636) (#115673) e1c818a1711 Merge pull request #115655 from microsoft/rebornix/fix-jupyter-activation 761dd469c13 chore: bump distro 8d779a4fada Merge pull request #115609 from microsoft/joh/fix115353 30fe91131c9 Merge pull request #115611 from microsoft/isidorn/selectForCompareUntitled 8dbf3d717b2 Revert do not show compare for markdown preivew bc38ed1b5a3 fix https://github.com/microsoft/vscode/issues/115353 65006668d07 Merge pull request #115547 from microsoft/aeschli/installProductIconTheme b99487f4168 Fix keybinding for Search view missing from view and sidebar (#115558) feda473d978 Enable 'Install Additional Product Icon Themes' 565dc9704f2 Use label as tooltip fallback properly (#115537) c02643e3c95 Properly set port label from ports attributes ae05392454a Merge pull request #115536 from microsoft/isidorn/debugConsoleCheckmark e4a65573a89 render "Debug Console" action after a separator a62c65bdb74 ignore focus when toggling debug console visibility 4d98741552d debug: do not render checkmark in view menu for the debug console ad232b0ac13 Fix #115509, register a separate action for opening serch editor from view (#115514) 4eb39372949 Fix #115511 Search Mode setting does not open editors unless search view is visible (#115513) 861a88ebadb Fixes #114201 da2adf433d8 Fixes microsoft/monaco-editor#2327 52f437953d5 add my paper cuts 090200d5aea fix https://github.com/microsoft/vscode/issues/115402 b36f9993162 Revert "fixes #114908" 99c406699ff fix mixed cells 384ef202510 :up: vscode-gulp-watch (fixes high CPU on Apple SI) e5b02b0610c liberate papercut usages 5a52bc29d5e Update working for default external opener d27b3130d92 Center notebook actions (run, stop, renderer, collapse, etc.) fixes #115087 38783a261a6 update version 36cabc4c123 🕺 One more time (refs #114219) 9934dea6888 Fixes microsoft/monaco-editor#2175: Improve hit testing code on FF 9d9aebd2e64 Add vscode-emmet-helper and restrict some labels 76adcde8743 Add `onDidChangeMarkers` (fixes microsoft/monaco-editor#313) de3b3ac5496 Don't exclude all unless there are no includes at all 791930308f0 Fixes microsoft/monaco-editor#2171 29c5c523023 Also apply #114709 to the extension editor 8c5e805d3a5 Fix spelling 662a698ef9e Skip failing test #115244 4d0a3637f29 Fix #115318: Getting started "Tweak My Settings" resets the getting started page f54b4fe5771 Fixes microsoft/monaco-editor#2168 d7821d5fb05 Remove console log ed4bd04c904 add other duplicate label to my endgame notebook 3fe4b0eb2f7 fix #115293, #113933 a7c0b43e1b5 Fix notebook action descriptions d045bc6ed1d Update color name for 'notebook.inactiveFocusCellBorder' (refs #114219) 660d6b82f85 Use unix style paths in includes always 4a338fd0d44 Merge pull request #115269 from microsoft/misolori/notebook-inactive-focus 588c3c49509 Merge branch 'master' into misolori/notebook-inactive-focus 12b56d878be Update color token name 'notebook.inactiveFocusedCellBorder' => 'notebook.inactiveFocus' 7ce63bef177 fix #115290 cbcfaa00f45 fixes #114914 8e22ecf4c85 fic unnecessary import e86befc8a9f Status bar: setting invalid color sets opacity 100%. Fixes #115292 b8bff49c9ae update distro 33e804f211f update milestone 3b87c36e2dc fix https://github.com/microsoft/vscode/issues/115207 631802d5cec comment out arm snaps 8aab6bc62d7 fixes #115219 3da57861612 fixes #114908 bb226913c5f Fix #115240 c8a90a48267 Merge pull request #115302 from microsoft/sandy081/remoteCLI b2a34770f01 #115294 also handle medium exe tip 724888adc76 Fix #115294 05568474922 remote cli: show host label f10dc2a548e more polish 4d3b15fda09 make sure to cancel continuation of `_handleEditorChanged`, related to https://github.com/microsoft/vscode/issues/115219, fyi @joaomoreno 971fa2cc9c6 Fixes #115304 0a943766a5f Fix monaco checks 9545d582360 Do not use the global `monaco` inside the editor bb841e3bbee fix https://github.com/microsoft/vscode/issues/115301 2ce26643d85 have a separate setting code cells in breadcrumbs, hide code cells in outline by default 17c617039b1 Squashed commit of the following: 3a287ee1eae Fixes microsoft/monaco-editor#2302: Only define global monaco if asked to do so or if using AMD a2bcb0608f1 fixes #114804 9519a5cb5a5 :lipstick: clean nuget.config file dcf0c56a796 node-debug@1.44.16 1d15b2fe17a use final DAP spec 1.44 6be5904d199 polish 1c1df3eaee5 fix #115050 flickering tabs when wrapped (#115273) bb931908832 fixes #113703 5d63134de9e web - fix compile 4b5a9c4b5f0 change remote cli to manage only remtoe 19cbd870aaf web api - expose env as API from facade (#115295) ee4516a4851 always on trusted-types for yarn web, fyi @bpasero 03902d48417 Revert "fix #113933." 70de88696c0 d'oh, forgot to adjust anyScore to new data format of FuzzyScore, fixes https://github.com/microsoft/vscode/issues/115250 7a9e56510d2 Not requiring NPM for typings (fixes #115228) fb5bc5dd2ce fix typo: ResourcEditorInput → ResourceEditorInput (#115208) f73c011ae3f fix: disable rosetta option for universal build (#115280) ee7e0ad0933 fix #115107. 5e5f2f3b6ba feat: add identifier for universal app in issue reporter and about dialog (#115277) d42bed7957a fix #113933. d09508d9cb8 re #115012. e48b3d3cad5 Update codespace-related getting started images & wording 64c4f7f49c8 feature insights for notebook. 5aac4f44562 fixes #115267 014aaa1047f Fix #115271: Search in Open Editors triggers errors when done without files 848896a75eb update distro 98da2b151c1 Remove unused variable dfb86c5fa13 Add 'notebook.inactiveFocusedCellBorder' color token b1ba0c70eaf Fixes #114172: adopt latest loader 4febf1e6c38 Add missing class to fix view items For microsoft/vssaas-planning#2286 4378b7f0201 fix #115169. e6d5a209440 Fixes `getBranch` when name is fully qualified ba428fe1029 fix #114225. ed8043effb7 Save only open editors toggle state 8dcebbaf54f path => fsPath again 949a20d14f7 Merge pull request #115253 from microsoft:jackson/open-editors-fix 20f8e59d696 fixes #112347 a1bdcf6aed3 fixes #115121 3d66ab98eb9 path => fsPath e3b0eae7403 Test fix for #114795 22960ca0155 fixes #115073 e375d137174 fixes #114869 cdbc22a9cbd Fix #115234: Cannot view search results when one is in an Untitled editor 977b2f6a1ca fix https://github.com/microsoft/vscode/issues/115201 ff27ea9437d Fixes #115148: Indent new line when using `IndentOutdent` and `appendText` e4f1833d79b Use x64 node for darwin-arm64. Fixes #115009 96fa81bb74a Forward arguments when click is invoked via `makeContextAwareClickHandler` (fixes #115026) 4bd2d367e7c Remote smoketest timeout (fixes #115159) e4022fb0e27 Expose a `TestCodeEditor` that could be used by the workbench tests 030c9d52233 cap notebook outline elements at 64 characters, fixes https://github.com/microsoft/vscode/issues/115199 ee65f21c4f7 use marked lexer to find headings, render MD as plain text before, fixes https://github.com/microsoft/vscode/issues/115205, fixes https://github.com/microsoft/vscode/issues/115206, fixes https://github.com/microsoft/vscode/issues/115118 c789c22efa7 Fixes #115224: Adopt Trusted Types in DiffReview a552ebc1f8b Revert "diffReview: use insertAdajentText instead of insertAdjacentHTML" eb1cf4b0bae diffReview: use insertAdajentText instead of insertAdjacentHTML e06ef891b70 Fix #114379 096d70ce18e fixes #114990 35c5689d292 fixes #114914 0f59f684a1a Fix #114982 bb6014df3b4 tabs - let the overflow gradient not draw over borders (#115129) 676e8d62a3e chore: remove universal build from its separate stage (#115203) 99e7aea4a82 PR template - remove pull requests link full stop (#115090) c246b5704f7 fix https://github.com/microsoft/vscode/issues/115124 7b0cfdd04ad fix git askpass 93830fbe3d2 cleanup #113562 8e68e0c4331 log source f434f853aaf pfs - workaround broken reparse points on windows (fix #115113) 1718be68d0f Fallback to default opener when selected 5662b3b6a25 Log exceptions and continue in calls to ExternalOpener.canOpen 9ee751e859d Error if registering an unsupported scheme for external opener 9421e50aa8e Improve documentation for ExternalUriOpenerPriority c4c5f45ce0e fix #115150. 39d9b04f2c3 re #115018. a56bc0c6711 Don't write preferredLocation into workspace settings for find file references 0ede5243261 Reword multiple external opener error e5c4f426fee Fix setting decription and remove unused setting d9e4f5cf97b Add new icon for open editors view 005db8394c4 throw an error with a clear message when a notebook document is missing, https://github.com/microsoft/vscode/issues/115018 0a0537961c7 tree: do not break fully when you can not collapse an unexisting node bdd2f1628e2 callStack view: set callStackItemType context properly b7e87c1bf27 :lipstick: 9788e81e98d Log individual events because `extensionIds` gets clamped sometimes d5bdb0efb21 fix #115011. c3746fa0aed Fixes #114983 21e970473c3 Fixes #114998: Fall back to a document range semantic tokens provider 58624bfcb16 Fixes #115032: Add description db92af7429d Fixes #115033: respect indent action when using appendText 774f887a985 update notebook 578e2dc4ee7 Fixes #115026: Adopt `KeyboardEvent.triggeredByAccelerator` c1afd7c238b Add `_debugComposition` flag 9e07bfd5946 related to #115037 75ff8b11310 fixes #115037 1f1ed78f7db Use ;; instead of ; for line comments (#115036) 960a93f0e2e add unit test for #114971 09ef3735975 Merge pull request #114972 from enagic/master 371629106b3 #114031 fix user data sync tests 08a2f9c5e36 fix enablement handler 0540478dc72 Reduce getting started for codespaces 1a9dd758530 Don't error out on unknown provider. 25f6e3e2540 debug: update js-debug a331c2b515e Revert test changes 684459c8dc6 Refactor searching in open editors logic 17685a9db95 fix #108950. a46fd0ee934 fix: update `isUri()` to compare `fsPath` as `string` 8fa1417e309 run oss too 1.53, update distro 88af66bceed testing: accessibility round 2 500d514ecfa Add fall back to default option for when external uri opener fails 8b1feaed3e6 Documentation clean up for ExternalUriOpener 0e5b47fa43c Use object instead of array for `workbench.externalUriOpeners` ed98eb19685 Fix spelling ae8bd3216f0 :guard: tests for selecting kernel. 2f6c928b209 Fixes `getBranch` issue with remote branches Improves perf by reducing git calls from 3 to 1 748b2e5a854 Remove `#` from typescript deprecation warnings (#114787) 60f3919b26a fix #114959. 7e3d5a0ce0f fix #114048. do not update active kernel if kernel is executed. c7cf663d0c4 fix #106362. Keep runstate when undo/redo cell. b112052169c fix #114171 8d7740fe3b3 Merge pull request #114944 from microsoft/merogge/integration fd1ba8c9692 chore: bump electron@11.2.1 2433b0eaf3c update distro f21a3b95e6a Merge pull request #114854 from susiwen8/hover-scroll a3131249625 update distro 58e88ff3ac0 testing: accessibility in explorer round 1 72172ed06c6 Merge branch 'master' into hover-scroll 68b7e79867e fix test-product icon 89e152635ab Limit spin to sync and loading (for #112298) 4ae47297a3e Merge branch 'master' into merogge/integration bd7dff7f071 testing: add test result to proposed api, ids for correlation ff08b2509f6 testing: fix not subscribing to first open workspace document cb69f5c9bca testing: add line background decorations 582ea371c2b [microsoft-authentication] Extend authentication session to return id tokens (#114675) 5a3fedf0c45 Merge pull request #114628 from microsoft/alex/python-language-configuration dac8d916d56 fix #110936 62093ff022b [html/json/css] update services & dependencies d877e86bdd3 Merge branch 'master' into alex/python-language-configuration e451364da15 Update Codicons version 51d19635946 reset template display when switching models. b18634fe902 fix #111587 Apply enablement to command links in welcome views (#113841) 07c3e907eb6 Improve glob module to support OS separator agnostic matching (#114810) 7468a060129 Revert "fix #114171." c708e3d5cf0 Move `workbench.startupEditor` to RESOURCE settings to allow setting to gettingStarted on a per repo basis Make sure to disallow setting to readme via workspace settings to prevent tracking attempts. 644d108f0d9 Do not reveal `FindOptionsWidget` all the time (fixes #114630) f3136a25fab Merge pull request #114934 from microsoft/merogge/terminalDimensions 60bce0f6287 feat: add macos universal build ci config (#114446) 19d87449a7b Add option to override 'pinned' when setting list selection Fix #114651 88fd9d9d178 fix #107239, set terminal dimensions d21d85a3fc2 Fix precommit hook on Windows c79a0282da3 Fixes microsoft/monaco-editor#2301 95227b3e10d Fixes microsoft/monaco-editor#2311 bf93e45b9c6 files - document file modes we use 1eb89d7da18 Add telemetry in the area of reconnection 3b03189afae Restore extensions in core (#114921) 5fcd9f74845 update distro 1aa795f2ff1 update jsdoc for #114908 415398e3995 tests - only use 'junction' for folders 604e231d371 fix strictEqual problem 7a89df95922 some more strict asserting tests 3cb3624be97 fix build b2242cc5ebf rename notebook outline settings to `notebook.outline.showCodeCells` fa7d5e7def0 fix `breadcrumbs.symbolPath` setting 830a7589e2a tests - enable symlink tests for windows again via 'junction' that do not require admin privilges 14cc5263711 Cannot open a remote workspace via --remote syntax. Fixes #114900 e60e0eab352 Cleanup some file related unit tests (#114895) a38cc82a154 fix mock, fixes tests 5592ed02fb3 rsource -> resource (#114837) bc3a770b78e perf - suggest status shouldn't listen when hidden dcda88e5a69 fix https://github.com/microsoft/vscode/issues/114798 eb5f9becd90 make SubmenuEntryActionViewItem not eagerly fetch/create the actual actions 137b6f5a464 Update endgame notebooks da0a04fffb9 Copy function fails over dangling symbolic links (fix #111621) c8ff3471b59 Do not attempt to open a workspace/folder that no longer exists when reloading window (fix #110982) d2cbc66835f Add empty problem matchers to build tasks so they don't ask me questions 1f8b429635d chore: custom protocols will also be intercepted by webRequest 62fcf3cce7e chore: cleanup webrequest filter for svg files 228459cc63a Fix: show hover when mouse control hover scroll 79be0a07248 Fix #114794 7b317afd931 Fix starting scroll for URIs with fragment (#111126) 98ec233c77e Fix: release note don't use editor style (#114709) e91fd3bd40b Updated Getting Started images with transparency instead of blur f0bd7eee100 only create processEnvironment once, now tests pass Co-authored by: Daniel Imms 4300e6c7d2c Fix #105177 get terminal environment variable to show up in remote container a095d7fcf74 Make sure altClickMovesCursor gets refreshed c2d09aaeac1 Open Language Mode picker for new file (fixes #110330) 885e66edf9e Open Language Mode picker for new file (fixes #110330) cd6fa35fb94 fix #114233. add5b32d959 testing: initial implementation of test decorations 3e55989cca8 testing: move test filter to action bar 2c19f7fb988 Fix #105177 get terminal environment variable to show up in remote co… (#114721) d8a3c5f61c8 fix integration tests. 88d66caf686 fix #114782 ed72c64b87e Remove unused constant de11a7dec60 Rename `isEdge` to `isEdgeLegacy` (see https://support.microsoft.com/en-us/help/4533505/what-is-microsoft-edge-legacy) 007f704eaa9 Remove IME special cases for Edge Legacy 4fac328d03d ok I'm out 86d96faaaf8 extract menu entry view item creation logic, fyi @joaomoreno prep for https://github.com/microsoft/vscode/issues/114123 bd929b33de6 logging - put storage tracing behind a flag to reduce spam b9c67304807 add some docs for workaround for #114227 a07327a430d better default for PeekViewWidget#_getActionBarOptions 3f3b4136060 maybe this is it 4b7f41a732e Merge pull request #114544 from microsoft/aeschli/114542 67c9ab0d514 test all extensions if system/builtin a4a9a5e69db debt - use css variables instead of dynamically injected style sheets 534d5b08948 :lipstick: 89855f0fcdb web - editor context menu sometimes wrong (#109166) 7bb55c99943 Merge pull request #112169 from chenjigeng/fix/debug-auto-decode-link da48ddc5fea upgrade gulp-atom-electron b57017797cf debt - adopt some strictEqual in tests f123c904b17 remote cli: do not sync installed extensions 5809e9eb031 testresolver: support server extensions dcc1e9df991 workspaces - shuffle some code around b3807b04f22 Merge branch 'master' into fix/debug-auto-decode-link ebf351d04b3 workspaces code cleanup 4937aee5ed5 repl: on debugConsole.wordWrap change recreate the tree, do not require a restart 1e0f94f9009 Fixes #114468 6f9c818900d :lipstick: 0d8ec8f09fe Ports attributes open -> openBrowser 1be6d22ebca add setting `outline.showNotebookCodeCells` to control if code cells should show or not 37c162ed6df revert 37a4b96ae18 simplify FuzzyScore structure, fix high, low match 9939537ea44 debt - use main in main side services consistently (workspaces) d7ddcd1e08c debt - cleanup WebFileSystemAccess#supported 6b1675af7e7 Open Language Mode picker for new file (fixes #110330) affac2b5ae0 Fixes #104004: Do not attempt to run extension tests in web worker extension host 2ef14cf785a fix workspace tests on windows 1848d3111fc Add workaround for #114227 ed00aebc389 Link names 98b4661b002 :lipstick: fed0eb5fd9a web - remove old API interfaces c7cb19ed216 Merge pull request #114749 from microsoft/ben/folder-id e5dd4b6e4b1 Add mock support for public ports to test resolver 4c0a4179e6e workspace - more tests for identifiers 106f26b27bc bulkEditService: dispose of listener in finally 1285843e55e When a tree resource has a tooltip it takes precedent 3ee49fa3f86 explorer: when new file system provider registered set whole explorer input 05bf7b0afcf Merge branch 'master' into ben/folder-id ec337988dd6 parseUri/Path => resolveUri/Path f9d16c3b3c2 fix tests 3f26fd17ba5 workspaces - some final :lipstick: cf4c4a0ece7 drop gulp-cssnano, use gulp-postcss b87d56c5332 distro 2e5034a74b2 Retry downloading playwright when hitting ECONNRESET f1e62c1190f upgrade dev dependencies 9321b2f141b bulkEditService: veto shutdown if bulkEdit is in progress dca2d81c652 upgrade build azure-storage 70a3118892b Allow svg files to load from Schemas.vscodeRemoteResource 4f2341834e6 fix nls problem, fyi @JacksonKearl 70f4451001a remove unwanted #region comment from vscode.d.ts 2fd18ac793f workspaces - reduce stat calls e61d0ba267c mark secretState field as private 4b9ccf578c3 fix #114727. load preloads when switching kernels. 167b920831c Enable searchInOpenEditors by default when not in stable 7e55fa0c543 Search In Open Editors (#107756) 9f9d1a76d97 support kernel id and extension a8145f67dcc testing: polyfill test heirarchies (#114601) 572bc1810dc Make sure we dispose of the open with picker after an item is selected 542de8e0093 Move schemes to opener metadata 885585c7f87 Remove test math formula from readme 793f2e06af4 Fix weight of terminal search workspace keybinding 3d641d9d35b fix #111889. d972bfc3266 Update elliptic and nwmatcher (#114670) 7310b17e25f Merge pull request #114669 from microsoft/merogge/altClick dec03c4a714 Improve doc wording 219d323100f improve setting description and make setting true by default and add === 821afe5e929 fix #111885. avoid duplicated execution placeholder status bar item on split editors 59fac4862da Merge pull request #114665 from microsoft/sana-gettingstarted 3003bde2214 revert "Open" to "Focus Terminal" 4b9b2ab9a60 Fix #114707 dbd4ede23f8 add api lint rule for region comments 41d8bb26110 Merge branch 'master' into sana-gettingstarted e7d3eb87cb0 :lipstick: 565f3a59e6c @ for CI failures 98ee1c6efda update distro 3ec90672006 Simplify local port logic in test resolver + OS check f2b2854a6b2 (for now) have tab decoration off by default, https://github.com/microsoft/vscode/issues/49382 67f1ada71a6 chore - a few more strict assertions in tests e8f6c273819 Use random port if privileged in test resolver bdc3b07f428 Fixes microsoft/monaco-editor#2305: Account for padding when computing the content height and having scroll beyond last line enabled fcccc85ff97 Add more to test resolver tunnel factory and fix port filtering ad437ef958c Fixes microsoft/monaco-editor#2313: navigator.clipboard is not defined when loading not secure, via http d3fbbece829 :lipstick: 5a95cd26e4f Fix #114708 3ad7af3ab6b :lipstick: 9b7323a7efe cleanup types d8831220ff1 Update showCandidatePort for test resolver f6490bfa5fc :lipstick: getFolderId 758f66b5986 workspaces - actually use workspace 79230501646 Don't await remote env before setting up process manager 9ca50fbb47c Merge pull request #114702 from microsoft/tyriar/109600 4877478fb6e Hook up alt buffer active ctx key b5f36a24b63 testresolver: start a test server a49455b5286 :lipstick: isCurrentWorkspace af915f0bc4c Create terminalAltBufferActive ctx key 6755b6bb3e9 electron - need to check if window is destroyed before accessing webcontents b1cb3b1cbb1 Merge branch 'master' into ben/folder-id 37ea1d82847 Merge pull request #114660 from microsoft/isidorn/async-tree-diffIdentity 19e390d5b58 polish 04ca5c80d3f Merge pull request #114593 from jeanp413/fix-111572 0249c31a59b testResolve: more tunnelservice fixes c735c8b2913 Merge branch 'master' into alex/python-language-configuration 67bf6577b4d testResolver: fix for tunnel server b6f19ccf3fa CI notifications 7d5052a8fce Merge pull request #114687 from microsoft/alex/ci-windows-cache 15e58cea4d3 test resolver: add tunnel server 18c8a3f0482 workspaces - compute workspace ID and check for existance in window service e9967519db1 chore - strict asserting in snippet tests 515f179c98a decrease repl refresh timeout f63310750f6 Even more tunnel provider logging 44e020ac02b Trigger CI 05e4d593c23 workspaces - move id computation to main f5d760b48f3 Merge branch 'master' into isidorn/async-tree-diffIdentity 831d1942874 Create .build directory e8473247567 distro 4f98d9c0be7 no double reveal of notebook symbols 232052d7e63 Create our own node modules archive (#114516) 33789a59919 workspaces - drop duplicated payload interfaces dd84387f9e6 window - merge workspace and folderUri into one 0c02f245f07 bust node module caches 914d8dff29e add notebook for notebook paper cuts c71edb7d883 get marketplace extensions in `yarn web` f4a0c209bea formatting c8ce53e492e window - reduce to one property for opened workspace f19f4a1b908 Fix Keep activity bar icons stable on reload (Web & Remote window) #114144 9cfba546810 move build/dependencies.js to typescript acaed317263 workspaces - add identifier to single folder identifier 7e2d8b48e36 Merge pull request #114581 from microsoft/chrisdias/solongsofar 3207692dbbd Merge branch 'master' into chrisdias/solongsofar 44eb775d1db further remove single workspace identifier traces 46b964b1b7c workspaces - remove ISingleFolderWorkspaceIdentifier requirement from workspace service 47a6682df6f fix: allow svg from devtools scheme eaaf647c8c5 workspaces - remove ISingleFolderWorkspaceIdentifier requirement from history aa774aeeb79 :lipstick: regions 71feb05bfba :lipstick: workspace payload 6b241a6845d Closes #111210 - adds openRepository api 5bcd2220750 Fixes checking for rebase against wrong branch Refs: #1866 1a4b35c2023 :guard: 26aaaeb11ca re #114583. 6e56202803c pin notebook editor when execution triggered. ece4eeb0647 only handle override when id is provided. a2830f41653 execute notebook with args 1c755a4fca2 fix #114674. 9a55eff36fa testing: make filtering work correctly 847c52e69fa testing: add hover titles for items 0e9e4e46774 testing: show stats about the last test run aa14d823df5 testing: show badge for running/failed tests c2a2e9cabf8 testing: show test progress, implement result service 48c7596e909 testing: fix swapped run and debug actions c6e62500779 Merge branch 'master' into chrisdias/solongsofar 1123ea5791b set false by default and consider multiCursor de24392e690 fix #114583. 79ec33ab1eb Merge branch 'master' into chrisdias/solongsofar f087f82a3b8 allow reopen notebook with another view type if not dirty. 559a63373fc fix active notebook editor in repen with quick pick 3fef8c795c5 Update gettingStartedContent.ts d591739670c Fix #97564 (#114438) 5717c0396ad update distro 1509770d10d Fixes #58440: Finalize `OnEnterRule.previousLineText` API aab5336e534 polish 9b1d85bad8c repl and explorer adopt diffIdentityProvider be4d10efa1b async tree pass on diffIdentityProvider to regular tree 28b221faa8b Bump concat-with-sourcemaps from 1.0.4 to 1.1.0 (#114648) 2e40c684ecf Bump fstream from 1.0.11 to 1.0.12 (#114649) bc3a873ee6f Bump macaddress from 0.2.8 to 0.2.9 (#114647) f9109f4464c Bump sshpk from 1.13.1 to 1.16.1 (#114645) d3965a2b4dd Finalize secrets API, closes #112249 699b02d3ae3 Bump hoek from 4.2.0 to 4.2.1 (#114643) 93ae815ba14 Feedback on secrets API #112249 64fa272029d Bump stringstream from 0.0.5 to 0.0.6 (#114618) 1266a4e4d05 Support git-cmd.exe as a git bash shell cbbf2d09904 Correctly resolve mapped drive on Windows 7139a93a8c7 Bump mixin-deep from 1.3.1 to 1.3.2 (#114619) a011dab93e6 Fix #114639 ec1eda0d96c Rename `OnEnterRule.oneLineAboveText` to `previousLineText` after API call feedback (#58440) 38c051bf865 Catch errors in tunnel providers and log 947626dfa4b fixes #114616 bab7a83909b shared process - check for destroyed webcontents before calling postMessage b0883ec87a9 Properly call dispose in tunnel factory We really need a lint rule or something for awaiting/not calling functions 02f7983156e More tunnel logging a7980b630c0 some initial :lipstick: 0b038406a95 :up: distro 6c4203f7482 Modernize CLI main (#114623) aaf5a7fee3b Merge pull request #110912 from Wscats/enoyao-Environemt2Environment e2bce32da4b #114627 complete fix 0fbab387483 :lipstick: strict assertions in extHost, mainThread tests 5cec4e2da6e don't expand outline tree when just updating, fixes https://github.com/microsoft/vscode/issues/114386 845a4d4268d add new rule to enforce Thenable over Promise, adopt in vscode.d.ts and vscode.proposed.d.ts dfc8f5ab91b comment-out console.log fyi @connor4312 e9263cc8269 some API proposal for open editors 543af670531 fixes #114607 2d5f7fd0726 Move `onEnterRules` to `language-configuration.json` 87dba0db6b2 Fix #114627 368c03fdc8a Fixes #114348: Allow `onEnterRules` to be defined in the language configuration file 390dac56a51 debug dynamic configs: Use the type of the provider, not of the config since config sometimes have subtypes f9f87fb6fa1 Add logging for tunnel creation 37c4d4b0a83 fix https://github.com/microsoft/vscode/issues/114621 b675fa18cb4 'Resolving your shell environement is taking very long' shown in every window (fix #114622) 06ab012baa5 state service - actually implement interface 9deba1b10ae code catchup 4d0d36c6ab1 update distro b31660dccc7 update distro f3c865334d4 use real tsec instead of vscode-tsec fork c0a0a35a87c more clarifying comments for shell env resolve 6effd9dce9c better fix #114564 a16beb16509 testing: fix run all tests command b50bd5d0944 Close #114342 1f8643ef760 Refresh Images In Markdown Preview On Change (#114083) 686cd7df530 testing: clean up actions, add run/debug all, rm duplication bb1c05e62e2 testing: unify testing view f37dd663235 Revert "window - do not send IPC messages to destroyed windows (fix #114563)" 16ea22eea19 Update Codicons: Add 'combine' icon 7f4d67c94cb Fix #110812 (#114553) 049735e8d96 A case for 'Shift+Insert' added. Fixes #114103 (#114520) a11dd7cd48d fixes #114199 3ed456050c7 Fixes #111572 6d6fec82092 Finalize product icon theme contributions. Fixes #113828 c42b385bcea Disable contributed openers by default in calls to openExternal fe81f9f5b26 Add link opening getting started task action. Closes #114582 3e4552ffcc7 Remove unneeded mapping from cintainers to disposableStore 3411ae55cc8 Allow splitting gettingStarted editors fixes #114321 fa2dbc16ca8 spacing 82a21e5a032 :evergreen_tree: 239213eaeb1 push it real good 3c4f06dc98a update distro 010e1d0e4a3 open in new tab 74f31a68598 Fix #111299 (#114441) 36cb0bde33a ci: disable exploration sync on PRs aef623dc1d3 remove "so far" from Problems message (which assumes you'll have problems later) eba7c23da0c trees: rename option to diffDepth b32d137681d Merge branch 'test-tree-testing' c100b5c26ac add clearUnacknowledgedChars flow control 6815e754602 Merge pull request #114237 from microsoft/smarter-indexed-setchildren bc84f07dc53 navigationActions: remove dependency to notebooks 81ec098e60e Merge remote-tracking branch 'origin/master' into smarter-indexed-setchildren 88835344408 trees: don't use diff identity provider for resort 7ae39d955d4 fix https://github.com/microsoft/vscode/issues/114576 ee4f4dbf97a make trusted types policy strict bcb5f3c77b7 update mkdirp fe1fdf0b4fe Use correct value to enable port finding 616fb1cfed2 Merge pull request #112317 from plainerman/fix-99072 c0c033ff4f7 Use port auto forwarding setting to disable port finding (#114574) b0b4bc4e338 add grammars scripts 15a285fd5ae Change "Requires Sudo" to "May Require Sudo" c6145fc3065 ext (un)link 4ff784e1fbf promise :lipstick: cf4111f6f89 show a modal dialog when no default formatter is configured, https://github.com/microsoft/vscode/issues/113903 dbf36e4cfb6 Change aria label when attaching f8df6a7e47d oops do not have .only 5a4d90a550a window - do not send IPC messages to destroyed windows (fix #114564) 635d7af6385 update ext types 7a34c6d6227 update tests 36929d3b59e exception widget: allow to tab over each link, enter to navigate to link 3c49afeaafa fix hygiene 867a52fc090 ext each: allowUnknownOption ff393a3349e add open tunnels to test resolver af2bcd4d461 bring back vscode-colorize-tests 0e7f3d0d8cb Revert "remove colorize-tests extension usage" 971190e4d0f fix region comment 81eccfbf68e cleanup native modules test 28ad78e7dc8 fix https://github.com/microsoft/vscode/issues/114537 af59db28c8d stream - some cleanup of observer 78d5286adb9 Bump sshpk from 1.13.1 to 1.16.1 in /build (#114534) 9af9580bf0a Bump stringstream from 0.0.5 to 0.0.6 in /build (#114533) b165e20587d InlineHint#hoverMessage becomes description (maybe better tooltip?) and support string OR markdown strings b47aa19443c FileService improvements (#114428) 2d9a0d12131 inline hints: tweak colors, react to theme change, add rounded corners to hints d29bb624a40 Merge pull request #113285 from Kingwl/signaure_arguments_label caa87e0b523 editor status :lipstick: 2472798cd57 Fix: selecting entry should focus back to editor (#114493) 32b28f6f8f9 And again bump distro ce106c3924a Bump distro again. ced398d18a8 Bump distro 30f17c9572a Merge branch 'master' into signaure_arguments_label dc588389507 remote install-extension with VSIX 18aa3199c23 Avoid `ERR_STREAM_WRITE_AFTER_END` 847300e49a9 support vsix for install-extension 92083ed3e18 yarn ext 085317e932e dev: ls d6ca7769f42 create extension workspaces bcf514160be uninstall-extension should remove both local and remote extension a40b4e72d08 add API command `vscode.executeInlineHintProvider` and some end-to-end tests ac85fb8a74b fixes #112045 7a938679f82 clamp font size at editor font size, don't go bigger d65ab8dcd9a use all of context decoration as decoration type key 4af282ea26d explorer: download report progress in the explorer due to rich download progress to not get double notifications 1d3b03bd551 padding should depend on font size too 4651f66cca1 simpler decoration type management (rely on internal ref counting) dab702a135e extManCliService: Sort listExtensions, fix output f101028176c Fixes #114299: Add commands for invoking semantic tokens provider 5087b08c6d1 :lipstick: 1981776d0f9 less state inside InlineHintsController-type, only have one type of decorations, and much :lipstick: d427deac780 explorer: adopt confirmBeforeUndo 74f272fbb7d update distro c27642c76d6 Merge pull request #114421 from microsoft/aeschli/remoteCLIExtensionsManagement 0d4bf785b6c remove hover (should come via decoration) and action/menu (should be self contained if at all) 97f237272c9 rename remote commands to _remoteCLI ae67879ed5d don't propose new API on ThemableDecorationAttachmentRenderOptions 756337d48af Merge branch 'master' into signaure_arguments_label 2bb41a14025 dev script 2388c80c74c Add `confirmBeforeUndo` option on the undo redo element 407557ca234 Save file dialog: sort file types alphabetically (#114487) bf90bd15185 cliProgressMain: add LocalizationsService to ServiceCollection 911a54273ce Merge branch 'joao/fix-web' 9995d128240 missing build output 09bc6fc64b1 Migrate to new deb repo (#114527) ed8655201ae fix web extensions 7fa8f1aa7fb Remove plug icon from ports view Part of https://github.com/microsoft/vscode-internalbacklog/issues/1689 667e41626f1 explorer: if you can not undo, pass undo to editor 85f1501c861 update distro d3611cbb634 fix toString 2ef04b24f42 :lipstick: c208ec384c9 :lipstick: move all scoring logic into _doScore 9441f1c6457 Add support for npm scripts with a space (#113840) 253d99a16f8 update distro 84865c05ecc Merge branch 'master' into aeschli/remoteCLIExtensionsManagement 22e02e00804 use URI for VSIX paths 96001455045 Fix tunnel creation in web 1c131cf2657 Avoid extra fields ebac10e0a56 Avoid conflict error 3c2c937f991 Merge branch 'master' into signaure_arguments_label fcc00b29f5e Avoid ts changes (#2) 08f3bcec33d fix https://github.com/microsoft/vscode/issues/114518 c16956439b3 Bust node module cache 5560c9f4da0 Fix #114455 0a2b6d4a1c6 inline collapse all actions 141b275c41f Merge pull request #114260 from microsoft/sandy081/comments/fix92038 ff309d2a239 Merge branch 'master' into sandy081/comments/fix92038 39edf4351b9 Trigger GH CI 12ef541b365 :up: distro ff9fbcb077b telemetry - lift some helpers to electron-sandbox 93b5a0591f7 sandbox - lift remote agent service to electron-sandbox 9cea4954aa5 notifications :lipstick: 691951c3b1c editor title - no need to update menu onDidRegisterExtensions ec5d1c2ab93 debt - push more window related things to window helper class eaa959d34b8 fix #114273 253e9e32261 shared process - consolidate services 0c8cf08b44b shared process - drop management service 1caaf1b2dbd :lipstick: path labels 61312f3708d Remove instantiation service accessor 2e89c2d4ba5 Add 'key' to onDidChange of secrets API, #112249 c5f0bac2a81 Create issue directly if signed in, fixes #95165 0faf1550289 Disable on enter test 48b726e39e7 Fix regex 5f6acfb68e1 Move jsdoc completion tests to smoke tests 800e173c403 Split ts into unit and smoke tests b813d5dd300 Leave the local extension host running when connection is lost to the remote extension host 3a9daf3e34a Adopt new vscode-userdata path format bec5afa2923 fix: remove unnessary asar files from mac arm64 a31b0617e24 expose altClickMovesCursor as setting (#114429) b2575665d82 Emmet wrap update, fixes #113930 21c11ba864c Fixes #114433 - adds setting to avoid git config 79cfca5aa29 fix #114416 LabelService.getUriLabel bad relative path if in root workspace (#114419) 5a25a566959 workbench.action.debug.start => workbench.action.debug.selectandstart a3febc56143 Potential new formatter for userdata in serverless. (#114296) 663532c3173 Skipping unreliable test a68f1326e87 Update built markdown preview code 308a4f6a484 Make sure ts extension has loaded before running on-enter tests d87041eddef Downgrade simple browser to prompt instead of being the default on web 3310d3ac2d4 sort notebook content providers in the list. 03dd7bf1d91 testing: polish and unit tests for the test tree 337b3e8d055 turn on flow control by default 7c4248780c8 adopt useCustom for permanent connection failure 2137a7f8508 implement useCustom in dialogService 5b8f78a1570 Move sync-enabled trigger to gettingStartedService 67c988005f9 Do not wait for the first reconnection attempt in the reconnection loop b64a4ae1aa3 Scaffold `MessageOptions.useCustom` 6d50c71f41c add editor command, fyi @dbaeumer ff042e9fa40 fixes #114203 381b99f6415 Also run the output based auto port forwarding (#114424) f7e7a95479e Merge branch 'joao/remove-grammar-extensions' bf764f1ce6a wip: sync-extensions dev script c198925570c extensionsManagement for remote CLI 4974a335112 smoke tests are tests too f745a912ae1 fixes #114420 62bb9b3d3fd shared process - adopt toggle method from management service 65582ba33d4 Fix #114326 0442b734227 remove devops ci badge d472f9d503c remove devops continuous build f8dbf7dd079 Merge pull request #114359 from microsoft/ben/shared-process-message-port 4af3c1c0576 get grammar extensions from marketplace 217aab28fac breakpoint polish condition context keys 6ca430e6a6f Merge branch 'master' into ben/shared-process-message-port 55325988a07 shared process - basic message port tests d952c818176 Fix #114379 6f9f6f806c1 Azure DevOps pipeline artifacts (#114405) d577c4b18da remove colorize-tests extension usage bc7d3c9ea6d remove grammar extensions be2732570ac Include tunnel service canElevate check 6889ed3ab17 Notification for elevating when using privileged port from openTunnel 25a9fcdb918 Merge pull request #114388 from jeanp413/watch-copy-value-selection 98acb74149d shared process - fix --status invocation addb6b9b53a :lipstick: 55e10fd785a shared process - introduce a separate service for management 22c1c0b486d update distro again ba7f5c60a5e update distro 5d620dc8466 Update Linux publish script 45e8d6ebc65 Update distro commit c082930a439 shared process - introduce platform/sharedProcess ec2a8e5b9ee shared process - rely on "close" event for disconnects 9e1863ec2dc shared process - :lipstick: 98d2d74ba07 Revert "Publish scripts update (#114375)" 99f0ab9f732 Publish scripts update (#114375) 2bf5b56f115 shared process - move the shared process back to IPC folder 8dff4cfa55d Expanding Getting Started text based on first round of feedback. bcb33ef6290 Merge branch 'master' into ben/shared-process-message-port 9c7128d8fab Fixes #114384 - recheck resources after save/add 746c455458c Respect multi selection when Copy value in Watch Expression View. Fix #114353 1894765dd17 Merge branch 'master' into smarter-indexed-setchildren 1a6eef3170b Update image ref 54cb0ed544e Clean up settings sync entry 4207c4ee13e Move defaultExternalUriOpenerId into configuration to avoid cycle 5c39159acb4 Fix cycle 4566eebe4fa Fix typo in markdown sanitizer (#111258) a34e751b017 Fix scrolling of markdown preview. Close #65504 (#111094) 64496f82196 Allow using 'default' to force fall back to VS Code's default opener 6cceb4eab08 Remove enabled setting and try to open simple browser to side of current editor 1e3a23b4e0a Fix simple browser button color for light themes cc5e8b22faf Continue work on url opener api a590d4fac36 Only show "Open in VSCode..." when isWeb. 67c889e3941 Merge and restructure menu (#114383) 56a6279a1c8 Don't use getActions in search view #92038 856277c8590 Github Login => Setting Sync ad3974ad88a :lipstick: e32e353bfde fix #114171. 255853d1714 Remove emptyWorkspaceSupport when conditions 11d18c2c094 `remoteName == codespaces` for codespaces section 9f3832dc688 Bust the node module cache 5029b4f362d Fixes #112552: Set server marks to `ITimerService` 9cb4f1e2ae4 update distro 43d111c0a48 Getting started content (#114305) a4b13661009 Add performance marks to `IRemoteAgentEnvironmentDTO` cbb94cfb607 Revert "fixes #114203" 8dfc81fedc1 Small tweaks ff7aabe3fca fix #114215 c8a6ddba9d0 Enable forceConsistentCasingInFileNames flag (#114334) 6525b42f479 remove unused file e12a9d74a62 #114144 fix remote explorer icon flickering d03490f3532 fixes #114203 f34a3ace3f9 update distro 55960b7d61e add flowControl to terminalConfig 7ae54ca2d6f breakpoint widget: use same mode for coloring as the underlying editor 835a1ce6efd allow execution against a hidden notebook editor. 5d6cba5cbc2 Reworking external opener implementation to allow configured openers to be called directly without a canOpen check 5b1e59c636c explorer: hide open editors for new users e1d8b926583 update distro 30f61c2449b part of #114214 serverSpawn=true c4d5b055d37 Merge pull request #114269 from microsoft/alex/fuzzy-score-improvements d4f993de63e Saving an untitled file closes it (fix #114272) e44fb4ab927 update distro 526f826ac14 fix #114192 634ebecb8b7 Refactor code to use `await` 3e6535d882c shared process - implement message port connections and wire in d6f27b92719 Polishing/fixing/addressing feedback for portsAttributes Includes: - fix in json schema - use object instead of array - change label of already forwarded ports when setting changed - fix for merging ranges c972009ef68 Merge pull request #114214 from microsoft/tyriar/flow_control cc8c9a2230d No need to store scores a79276dc649 Move to log service f8ec60aa06f Add flow control setting, remove fake latency 98038a8835d Merge pull request #114208 from gjsjohnmurray/fix-37570 065f0e46405 Auto forwarding fix 7a9bb5a44f9 breakpoints: inline action to edit condition. Render conditions for function breakpoints. Allow to edit conditions for function breakpoints e9f6c35c17c Fixes #114146: Increase max BracketSelectionRangeProvider duration to 5s in unit tests 4e4d2484a74 Extract `TestTextResourcePropertiesService` to its own file 00f8540d793 Fixes #114332 f1cb1b27f3e format 40e3106e5fa fix list drag affordance 7899bfe3eee Merge pull request #113315 from qchateau/fix-semantic-highlight db30147068f Add test for case to assert that fetch should be scheduled again when a text buffer change occurs while the provider runs and the provider returns null 44278132f4f fix peek view alignment cd906568752 shared process - document electron IPC 96b44121f98 shared process - add error handler and graceful-fs 2964fcbb846 shared process - extract more cleanup helpers to contrib f1c510b4a88 Merge branch 'master' into ben/shared-process-message-ports c265dff48a2 chore: bump electron@11.2.0 a8dd7f60a62 update collapsible when children change 710846866f7 Activate extension on simpleBrowser.api.open b7f9eddf043 Allow passing viewColumn to simpleBrowser.api.open 9b83eb6eb50 smarter depth selection a1d5ea876c3 Polish, also fix #113930 2b5ae783bf6 testing: add full json reporter to show more complete output d39eefd1b0d update distro 59891debcf0 Wait for outstanding zlib flushes when draining a WebSocketNodeSocket (#114314) ea13176ee96 Enable image preview for avif images 7da421d99bd fixup! make it work for compressed trees, recurse 6c4a00ce747 Make default text editor replace existing editors for resource (#112848) a59f30011c1 Add a 30min cap to CI jobs e7aa009ac3c Remove enabledHosts setting 61ec57016c4 Fix spelling 47aa3ad09ab Continue work on opener service d6936dd524c Add mechanism for snippets to overwrite Auto-closing pairs in some cases (#114235) 2ca7b5426a7 :lipstick: fb6a9b4824b fix #114289. notebook.selectKernel takes kernel id. 2156b8cc758 check and ignore not found error 31a15b5b9a3 Add command to kill server and trigger handled error 23be24d8289 Allow theming getting started page progress bars closes #114303 2774f79df3f Do not show "Cannot reconnect. Please reload the window" if the cause is a handled RemoteAuthorityResolverError 56e05127690 move to browser namespace e5e791003f4 Merge remote-tracking branch 'origin/master' into smarter-indexed-setchildren 2d892ae9c7f Fix arch check for PowerShell enumeration (#114292) 3ca55d031b1 Update Codicons: increase gap around plus icon (fixes #114016) b903748b833 Fix candidate filter and auto forwarding wiring (#114290) d66db5cc754 fix https://github.com/microsoft/vscode/issues/114220 c88a51e10a8 Merge branch 'master' into fix/debug-auto-decode-link 4450e1d827d Azure DevOps - publish Windows artifacts (#114285) 42d7d3a47d7 breakpoints view: render edit action inline for exception breakpoints 40d6f79875b MenuItemAction: make sure to respec item.icon a198be16f93 shared process - some :lipstick: 67f8c0ca5a2 Merge branch 'master' into ben/shared-process-message-ports daa7afebd06 remove undefined from outline data source cf03ef33f45 allow to clear input of data trees, fyi @joaomoreno 0ecb7735496 shared process - more cleanup dea0095e83e Add icon for public vs private ports 212a9434541 Merge remote-tracking branch 'origin/master' into alex/fuzzy-score-improvements 36a9cb8645e Improve `fuzzyScore` 475d3464e87 Have single outline config and let outline creator know for what they create outlines 9266fc49839 mock a label service to avoid breaking layers in tests 9af036b8274 Fix comments b4e4bd16421 Allow tunnel providers to support making a tunnel public 9853c8fe6d7 Fix cr issues a4f9e607619 fix https://github.com/microsoft/vscode/issues/114266 2376bed71e1 :lipstick: some region-comment polish 64f32932c68 debug console: fix error in console, do not bind to same htmlelement a scoped context key service 75ea87a2636 shared process - introduce platform/sharedprocess dcce02644ea fix https://github.com/microsoft/vscode/issues/109658 5ce7b02b6e8 refresh the remote indicator when actions change 5db4708e99d shared process - avoid payload IPC roundtrip and enable console based logger 6fef673683f update distro de4463874c4 callStack: do not use getActions() 152d0ec8f05 finalize CancellationError API, fixes https://github.com/microsoft/vscode/issues/93686 adb037b74d8 fixes #114137 e59dc77d0d2 shared process - more cleanup 3f37b664fcb adhere to DAP spec; fixes #114229 e776f87e140 fix --builtin 7285f791ee5 Use menu 1fae5211635 shared process - more cleanup 9bfa4c1d558 Merge branch 'joao/extract-extensions/themes' 9c6e10497b5 use in-mem fsp 7eb52e75e08 shared process - more renames eba7707d382 shared process - clean up some types and imports 675e5da76b1 shared process - expose methods for message channel API d0749f8c9a3 use in-mem fsp 936e77761b5 include error into startup error dialog (#112846) 68ba207260a List still dirty files when backup fails (#114064) d4be66da200 Fixes #112487 - avoid using stale cache for render d78fad382aa skip failing test on win32 (#114248) fd0a3a12e7b Update wrapper class name for paramter hints 8f384b51a93 Fixes #114204 - always renders the input box 27b824b32c8 Adds ability to pass remote/refspec to pushTo cmds d076ee1b943 Adds force push mode to push api 02380e70149 Removes repo hint from args 7f489f589d8 Hide warnings for settings groups that have dynamically registered settings Fix #113747 d7d5f20047c Fix #114218 145bcd3a732 Insert new code cell should always use available languages. d536903a2b0 :lipstick: d88b60ceca6 languages in notebook document metadata. 8a2b9e9047d trees: add diffIdentityProvider for efficient setChildren updates b6435bc4240 Remove unused import 47a135e715e Rework opener api proposal 6184addcd1d fall back to homepath if home unset. closes #112775 b3d57e69b02 Update PHP grammar, fix #113185 92833fca559 fix #114233. 04efea43fa3 testing: peek diff test outputs 989f2eb812e setImmediate => setTimeout 02276814922 Add new external uri opener service 22c88cfaaeb Batch ack events coming from client 3232112f9ba Only resume if it's paused 60e46eb8756 Delay animating until content is prepared to prevent weird flying elements. 0738f76daca pull themes from the marketplace 0a19f7702a9 Rename ackId to charCount bf52d50a0a3 Remove ackId from data events going to client 7aee462b8a3 Use char count instead of ack ids 7e5c01208dd Start of low-high watermark flow control 69a6e6ac937 #113757 show panel move and hide actions only for panel views 3a4dcf4890d Fix unit test for Win32 release (#114212) a04802f5865 #113757 show panel move and hide actions on view context menu f29502563bb #113757 allign reset location action f1ee68fc468 add tests for RELATIVE_FILEPATH snippet variable c6c7ddd4437 Merge branch 'master' into tyriar/flow_control 6430ee1efce Basic flow control for ext host processes 9d39f4e6cb7 don't auto insert semicolons 169269a3f07 fix tests 3767f97bc32 Adds onDidPublish to Git api 8832366467e Closes #110881 - adds possibly rebased warning eba4da27278 #113757 show sidebar actions only on sidebar views 2e279d37e7e Property preview text wraps lines in debug console 554ae13fa9a Fix address for port open attribute Part of https://github.com/microsoft/vscode-remote-release/issues/4046 b7b36bb1908 remove some tests 9ecba1b468e Merge pull request #114039 from microsoft/isidorn/bulkFileEditsUseTextFileService 1e9b86da1f9 Finalize adding a cancellation token to resolveTreeItem Fixes #111614 161ce44ddae Azure DevOps - Move release into a separate stage (#114205) d3e4bdb6177 Merge branch 'master' into joao/wsl c644f3788d0 revert wsl and distro 3a1c42c150e textFileService make getEncodedReadable public 67f9988bdc4 Support to define additional attributes for ports Part of microsoft/vscode-remote-release#4046 53be807cb4d throw nice error (and prevent stackoverflow) when instantiating services recrusively 3653f34dbab Do not instantiate hover widgets in the `onModelDecorationsChanged` event 0a28ec7fb14 fix #37570 add RELATIVE_FILEPATH snippet variable 868271067e6 filter perf marks that don't start with 'code/' 675638196d2 debt - remove duplicate drive letter implementations 4816a253eaa Revert "use PerformanceObserver in node's perf-util" 05c4659e096 use PerformanceObserver in node's perf-util e500f76d9cc remove test dependency on theme extensions 0324150670f Merge branch 'master' into isidorn/bulkFileEditsUseTextFileService d88c1b4a64c Merge branch 'master' into isidorn/bulkFileEditsUseTextFileService 22bd999e86b debt - use provider extUri in file service 4b3ab7048fc :lipstick: b90166177bf reuse stats collector for EchoRunner, fix missing titlePath-property, fixes https://github.com/microsoft/vscode/issues/114190 7a8c7f57312 activity bar - use IAction in more places b5b160e015e activity bar - show a "Hide" entry for accounts and home indicator (fix #113757) 7ab5c2a90ae activity bar - consistently show right click menu everywhere (#113757) f675564c5dc fix #114028 e17aea136d3 Fix #114189 - disable caching b9aaba047c6 activity bar - remove "Hide" from left click menus (#113757) 2b0132d09f1 activity bar - update order of entries to reflect visual order (#113757) b33b28dd078 Activate onStartupFinished (#110031) d61eb64745c activity bar - change visibility of entries to checkboxes (#113757) 6dc779565e1 :up: distro 913fff96a3c testing: fix error when test view is hidden after showing f5665378fd2 testing: start of diff peek view 09d99f7d71b resolve kernel providers and kernels. 732d4ff89e8 Make PowerShell 7 default if available and show in choose shell menu (#112768) 74038b7e0d8 do not use file scheme c88ab9e0b63 use in-mem fsp - remote folder config tests 506ae4a53e7 use in-mem fsp - multi root workspace config tests da3a21ee4e6 Merge pull request #112602 from microsoft/rebornix/output-view-model 041a5c3b6b9 fix tests - do not use file scheme f92251d8e61 use in-mem fsp - workspace folder config tests 1e44ae5da86 Merge branch 'master' into rebornix/output-view-model 03450bf0941 Update Codicons: Update '+' modifier location (fixes #114016) 84f2cf6449a use in-mem fsp - workspace init tests 722a6664f84 use in-mem fsp - workspace editing tests a2efefd3713 use in-mem fsp - workspace tests e3b18fa3efb testing: add filter box 676bb6b100e Merge pull request #114127 from shskwmt/fix/113603 990906a1655 Adopt strict assertions 00a781f926e debug: use mnemonicTitle and avoid dupliacte registration c38c1f497f2 use in-mem fsp - workspace folder tests 42221c900ba Set override to false when reopening editors after dragging them to a different editor group. This fixes #109000. (#114093) 31e33c478e5 Fixes #114042: Use Buffer only when it is available cef7004a46a activate search result extension onLanguage:search-result #110031 39619a136b6 minor polish e8fb4fd30da bulkFileEdits: use textFileService only for creating empty files c85297669ee Move id, label, and options to authentication provider registration e9ae0082963 use in-mem fsp in tests 3f3e35bf17e Add boundary for right arrow typeahead and fix bug with resetting Terminal (#113863) 61c6334a3f8 Fixes #112373: The hover should always consume mouse wheel events 3d500ebd8b4 Adopt proposed `CancellationError` (#93686) 5c1543b556c File name in editor tab reverts casing on save (fix #114096) 76c22d48c82 Merge branch 'master' into fix-semantic-highlight e149bdb42ac Merge pull request #113837 from HaoboGu/HaoboGu/issue113404 1551d1f1ff0 Fixes microsoft/vscode-remote-release#1485: Make sure to only render Reload Window prompt once c9bae24fb70 More UX feedback 7cf2ad082f9 Render remote name when reconnecting cba1d1b1848 Prefix all `performance.mark` calls with `code/` 64947067ab7 :lipstick: unit tests 533d094020f Allow logging FS access with stacks 8ec95fa3b7a tests - improve ext path tests 55bd92dd538 tests - extpath tasks are flaky ea7b8ddda3e Can't open, rename or delete files that contains ":" on linux (fix microsoft/vscode-remote-release#4227) e3f5b3dfe47 Merge branch 'master' into HaoboGu/issue113404 bd5c20448c5 Merge pull request #114129 from microsoft/alex/configuration-editing-tests-improvements c173fb7d72a #114144 revert showing cached theme icons 088304c9968 #114144 - Do not cache only uri icon efb833ab7c7 Merge branch 'master' into HaoboGu/issue113404 5755d943ca6 Tests must be compiled even when `yarn` is executed acb0a35629d cache icon paths in web 73b4dabb2d4 Make lint happy d1cfec44472 :lipstick: 8b288893a92 Merge pull request #114101 from shskwmt/fix/113807_parse_args d541d7c64c6 make SubmenuAction strict: don't allow changing its properties, make it not disposable ea756907598 - Make cached configurations not disposables - use workspace configuration disposables 31a53bb2427 :lipstick: 43310230886 Merge branch 'master' into signaure_arguments_label 175e2c0b1d1 `yarn` is already installed (fixes #114140) b57739f4a24 tests - more use of getRandomTestPath 64a14963f7b Expose actions to duplicate editor groups (fix #114132) 60bc00ff63d webpack config typing fix f6bbc8b68d4 update tsec tool cfb2ad879a3 Update src/vs/workbench/services/configuration/browser/configuration.ts e25cbb45e7a Dispose all Disposables from tests (fixes #114125) 2fd00ba9fef Add a way to troubleshoot fs calls 57c405c24c3 Change reason for moveWordCommand to CursorChangeReason.Explicit 7c3aacb40aa Add a mechanism to track disposables from unit tests 11ac71b2722 editors - cache previously used layout and return it 03cb2d2a236 Dispose `PieceTreeTextBuffer` instances e114a24d9f9 Improve usage of Disposable e71f31abe9c editors - copied group is missing to register editor listeners 313f4bfecdc fix #113620 db701d281f7 Fixed not to skip determination of option type starting with "_" 3ceb3a100ed tabs - improve logic of previously used dimensions and relayout 6bd7b70515d Revert "Enable webview tests (#114059)" 0a3a9ce7bb9 State of tabs is not fully updated when toggling workbench.editor.wrapTabs (fix #113808) b0e96922417 tests - selectively enable some previously skipped tests ea1b3f27db4 Fixes window border causes webviews to be positioned slightly off (#114061) 01c6003c295 Enable webview tests (#114059) a12a996d780 Retry idb tests. Ref #114025. 7a7d11fcc6d Merge branch 'testing-group-by-result' 5f2036033c0 testing: improve projection logic, add state grouping e3509b62fe6 Add search.mode option to control default search experience (#114015) d110d503425 Remove webviewHasOwnEditFunctions context 50dd2dd3f9e Add context for when the webview supports find d8a3dd44be1 Removed unused param 0fafaab62e2 mnemonicTitle in native menubarControl d95e22d0a8d make menubarControl prefer mnemonicTitle, make MenuItemAction only implement IAction 8d8ee957418 options for getActions() are optional c88c207e6c0 fixes #114028 6828ae1ab50 Removes trim & fixes regex c45eac1a819 tests - use explicit skip over handling within test 8d46328a407 more :lipstick: 832afd1276d :lipstick: 15cd2a1abd2 #115025 hash the uri and create css rule aa5064d4fd4 Lift some tests to browser (#114041) d6a63fc79ea FS improvements for unit tests (#114026) a2251a3b6b9 do not use mnemonicTitle for all action titles, #102361 3898af5db3d typos 10592747998 refs #102361 mnemonic as command model property 8ae693bee2c bulk file edits: make sure to use textFileService when creating files bebd0664073 Fix gulp task provider so that it doesn't always try to run 29e0cfd8beb Always elevate (if needed) for openTunnel API e7c84aab5b1 Fix privileged port elevation flow from UI 61187660af3 Fix #114031 114e38f175e suggest - add min height when persisting widget height 2042a0e4c23 Add new terminal link text for tunnels (#114033) 174259eec8f Log when lsof fails and return initial fe175afdb80 list widget should not remove rows from DOM when reusing 91a0d07f3e7 Improve `canTunnel` Part of microsoft/vscode-internalbacklog#1709 288e8c233a8 Fix #110525 5ed73f6e850 don't theme icon for MD elements 6d2e0aa21d3 rename tsc config file for better intellisense ec4c9f4c8f7 Merge pull request #112833 from homebysix/list-extensions 7b16f15d005 Merge branch 'master' into list-extensions a5a0c1527d1 add tsec config and exemption file (defunct?) ca6a7a69989 :lipstick: 5adcd2521e9 Fix #113257 ed6c343edb7 gracefull fallback for TrustedFunction 44c9b4bb7f3 use TrustedFunction workaround when loading extension sources inside web worker eb940d4ed6d Fix #113988 adc68dc3561 Add `canTunnel` to tunnel service Part of microsoft/vscode-internalbacklog#1709 128987f575e remove duplicate step 4c42e6c111b Fix #114013 35766c616cb #113757 show sidebar actions also on view title context menu e4dc7b4d796 Clarify OpenDialogOptions note (#113831) (#113998) eb409622888 tests (unit, electron) - set forbidOnly when running on build machine 691da329d66 Revert "refs: mnemonic as command model property #102361" 96e2981c91c skip failing test (#113882) 82dc292811a Merge branch 'master' into HaoboGu/issue113404 0949d5b794f Remove unused var acda4aed821 Make the external opener a two phase process aa73c2d435d Fix random focus lost issues on CTRL+1/2 for a webview (#111676) 045b0fc4c09 Fixes #110509 - handles markdown escaping & spaces 31d5e48d92a Fixes #114002: Finish writing any outstanding messages before disposing d36b3616e70 Change Emmet to onStartupFinished #110031 b041f460ce6 Show a disconnection dialog only after 40-50 seconds 455b029ad1a Render "Reconnecting..." in status bar and use "Disconnected" only for permanent disconnection 5d9e867aa4f refs: mnemonic as command model property #102361 3dc0203e021 testing: auto reveal selected tests e2c305f3a3b Allow registering additional external uri openers bdf57b45ced Remove button background in simple browser aa85ab9d03e Make sure we also log event when creating a iframe based webview d964664da29 Disable dynamic cwd resolution on Big Sur d68056d9072 Improvements to batched testing. Ref #113911, #109271 8c3f5dd3fad Support to start multiple debug sessions from a single launch config 80f369b7bee reduce number of entires in test batch. ref #113911 2b20162d227 Avoid hostname resolution in lsof 70e37bec1be Fix #113920: Codicon => ThemeIcon 16452c54f38 Merge pull request #113938 from microsoft/alex/terminal-exthost-improvements 98cc02c097d change default zenMode.restore to true bbb0aadc87d enable trusted types by keep a yelling default policy for a day to two, https://github.com/microsoft/vscode/issues/103699 23ac286dccc #113975 use insance to remove svgs tags e803459d4ad fixes #113921 8ad08b04f5e Do not compile `/test/` and `/build/` scripts during postinstall, the scripts get compiled explicitly during CI fa701a373b7 update references viewlet aa48a4eff7b `monaco-compile-check` is covered by the GitHub CI b1877cd33e2 Merge pull request #112033 from solomatov/error-handling-in-terminal-start a7cf03de2db Add elevation message to ports UI (#113990) efd298ccbdb Fix #113760 a78fffbdb01 remove unused eslint mocha 72572c59cfb bust the node modules cache 578c3d5374e fix #113781 d4c32800737 tabs - polish how to detect that scrollbar needs update 683a30f74c4 Revert "better fix for https://github.com/microsoft/vscode/issues/113852" 7222c005ca9 update loader, event better fix for https://github.com/microsoft/vscode/issues/113852 c1930b6baf6 make default policy strict, https://github.com/microsoft/vscode/issues/113975 cdb373186cd refince CSP for trusted types but don't yet enable it, https://github.com/microsoft/vscode/issues/103699 a9dc6d28fde use default tt policy to workaround electron webview innerHTML-usage, https://github.com/microsoft/vscode/issues/113975 d5fc23ce6ce Wrapping tabs: editor toolbar bleeds into tab when space is limited (fix #113926) 14bb2fdc128 Tunnels from a tunnel factory can have async dispose a84603f49ff fix condition f2dd0954925 add (disabled but almost ready) CSP for trusted types, https://github.com/microsoft/vscode/issues/103699 4aff4b99239 trusted types - loader should use trusted script url when using script tags, https://github.com/microsoft/vscode/issues/103699 de9e9c414ef fix path in tsec-compile-check df10825f69e fix yaml 5e673678ebd Merge branch 'joao/build/single-compile-job' 575f87306f9 Merge branch 'joao/build/esbuild' fb4a88e4037 rename ab3297dd136 only run terrapin on cache miss 11b79ba7ebb parameters d495358b01d add parameter display names d944b91cd1b remove no-exclusive-tests 39810d812ef Sandbox: adopt forcefullyCrashRenderer when reloading unhealthy renderer (fix #112485) 2b7435c389c window - focus() window that opens from protocolHandler af5adb530a3 Revert "add arch to cached data path, https://github.com/microsoft/vscode/issues/113852" d687818f8e1 better fix for https://github.com/microsoft/vscode/issues/113852 2d114755e83 update distro 4149b09417d parallelize eslint and hygiene in product-compile a4f9970924b Merge pull request #113826 from nrayburn-tech/issue-109438 74bc1d2672e re-enable test for https://github.com/microsoft/vscode/issues/111867 c6ceb1ab2ea isolate eslint from hygiene 519f8691bad fix typo 935cbe6aff3 update test cases 3759568789d support more unicode chars in isSeparatorAtPos 685999bcf35 Merge branch 'master' into HaoboGu/issue113404 e269e5e2c6c revert back using switch for isSeparatorAtPos, add several cases f3b2680ee3a Merge branch 'master' into alex/terminal-exthost-improvements 01089c0a505 testing: fix unit tests e2c91378410 testing: fix unit tests 59091157571 testing: code lens and diagnostic information for tests 3d8888779d9 Fixes #112446: Avoid timeouts in mirroring terminals to the extension host by assigning a temporary UUID to terminals created on the extension host side f1151f84ff7 Adds userAgent to clone, pull, fetch - #111909 Changes to use env 6802a656e26 Use cast instead of generic 271d9b8c007 better hygiene task definition 809d2f63d0e Added show options to simple browser open command 2fed7ba374d Adding settings to control which schemes simple browser is enabled for ba67d1bea44 Add more explicit type for TS 4.2 af6d164f73e single compile job cb67fffb94b improve hygiene glob patterns 27e26536f12 :lipstick: e4fe157544f equal => strictEqual f489602633a Merge pull request #113919 from microsoft/isidorn/workingCopyFileService 4aca944132c make sure to not fire any events for empty opertaions array 4f109404604 minor polish de8b6772761 Skip batching test due to failing on build machines. Ref #113911. ac10e57f6cf fix: The git commit message field is cropped with negative zoom (#112316) a7267aa0935 Update distro e8edff5eb7e xterm@4.10.0-beta.39 3259985c7af rename random_uuid to just uuid 1ace7e34995 remove console.error on commands for noisy tests 6763d82fdd1 also filter before debouncing when menus change 6c12d89415b :lipstick: 9293f64e937 :perf: first filter and only then debounce event handling 43aebefbf62 use Date.now() in stop watch c8d91038302 :lipstick: 39dbcfcfbce Set remote.restoreForwardedPorts default to true c6cc7d61401 workginCopyFileService: only one event for multiple operations 4d4b9225d2c Fixes #113917: Add square brackets around ipv6 addresses 83b4d6c8ce0 :lipstick: 7a27b248841 fixes #113815 8389f072696 Azure Pipeline - Adopt pipeline parameters in favour of pipeline variables (#113902) bb4cbce9699 Revert "publish linux builds" c49ef6df61d [html] update auto-rename-tag to linkedEditing migration 4fe3f75c6d3 Use `nodeSocketFactory` for tunnels for now (#113914) 1228854b4ae Adds support for gulpfiles using ESM. (#113505) 169a0ec047f sandbox - allow to enable vscode-file protocol via argv or environment (#98682) b1659198f62 fix incomplete stub, fixes tests 448d0497e68 Revisit how activity bar items and status bar items can be hidden by the user (fix #113757) ca2e7d8527f publish linux builds 0cb7dad36f0 adopt creation of N resources at once, https://github.com/microsoft/vscode/issues/111867 b99e9bc2990 apply file edits in bulks of equal edits, https://github.com/microsoft/vscode/issues/111867 841f6c9938e align source-map dependencies c8ef4ecd835 remove deprecated settings sync settings 27ff5760893 :up: playwright f054767eb4e remove unnecessary line 22b9a2b1e8b update esbuild f8a07fa6a9d Revert "build on mac11.1: upgrade version of playwright to 1.7.1 (#113906)" 63a0638d4cf build on mac11.1: upgrade version of playwright to 1.7.1 (#113906) 983bd69effc Update problemMatcher.ts (#113834) 0402f76ace4 Workbench has white background, then flashes black, then flashes white again. Fixes #112816 c6a1eda3315 themes: add ThemeSettingTarget d51e1fa4718 workingCopyFileService: create and createFolder support multiple resources e153ed6b3fc :lipstick: 16da2c5f0e5 change create, delete, copy, and rename operations so that they can handle multiple files at once, https://github.com/microsoft/vscode/issues/111867 fa593d83ad1 use vscode-tsec instead of tsec 007eb56ca84 yarn.lock leftover 6503475091f chore: bump chokidar@3.5.0 (#113886) 72095f86036 add arch to cached data path, https://github.com/microsoft/vscode/issues/113852 5ed76e3da7b Merge pull request #113842 from nrayburn-tech/issue-113809 296ba5464ba Remove obsolete chrome debugger recommendation aaa8fa92c67 empty f1a0aa9378d Fixes #113866 - removes transform optimization 9b6aaf1e86c Fixes #112481 - missing refs (because of trim) 4167837cb85 Show hover for comments, fixes #108396 dc5a3da3abd Upgrade to latest Emmet (#113848) 6f56b47c696 fix typeahead regex for move (#113850) 98f1ed6c758 Fix rewriteWorkspaceFileForNewLocation test. Fixes #113762 20950d65268 testing: update old import cfc8c5d3d77 extension webpack: do not copy .ts files as resources ed3989b069d Add some recovery from a missing compositionend event (#112621) 2c5c0a3be14 testing: fix loading indicators, add progress during initial test discovery 2112a99fd9d testing: migrate from actions to menu contributions 52bdc14cc6d Copy of translated errors for js/ts web build d29388d2554 Reenable test, fix #113768 485c5fc0093 Revert "extension webpack: do not copy .ts files as resources" 3aa41899aaf Unable to change to another theme if the theme file does not exist. Fixes #113661 66482b368eb Sequencer.queue: support failing tasks (for #113661) 63deed1c6e0 :lipstick: 41d01ec14e9 don't leak emitters of context key services 87d2b3d07cb add support for event profiling 45f79f85735 Scope gitignore out* pattern microsoft/vscode#113823 9be7f7fb655 file name incrementing for files without extensions ced3bb4bb9e Secrets API feedback 037708a33f6 Avoid textual codicon references in gettingStarted Fixes #112657 c8fbf3aca54 Fix #112735 0319fedae10 add lint rule for missing cancellation token in resolve and provide methods, fyi @alexr00 please remove surpression comment eed13efcc36 fixes #111413 d0f68b3ec9a IndexedDbFSP Perf/Correctness Improvements & Tests (#109271) 76dd27c3be1 fix https://github.com/microsoft/vscode/issues/113829 064fc7a5267 api comments, fyi @RMacfarlane 265bd0b5b9a Open Editor Sorting: fix issue when untitled editor changes name and list does not resort c1274981145 Fix #101738 3f435ff6c1f fix #113404 ab1aeb6b73d extension webpack: do not copy .ts files as resources 0a3e23a5b2f new html settings not it in the settings UI c41302c5ab1 Relax worker.onerror after a successful worker start 6c95b041ea6 test: add tests for #113403 c907b8fff85 no vscode-imports b99faeeb3ce fix minification target 51ea16966b6 :lipstick: remove appendChildren infavor of append, fyi @bpasero c46eca1dd34 Merge pull request #113518 from nrayburn-tech/update-dom 37b0d344c7b add a RANDOM_UUID snippet variable 2d1b63d6a3e Fix showing views submenu when view is merged with view container and view has secondary actions. 48dbb328998 fix https://github.com/microsoft/vscode/issues/113819 e8ad195f3e0 Revert "revert pool" 13e313c2b58 fix compile and hygiene cache miss f06ebe74253 :lipstick: f90c518b03c debug: remove enableBreakpointsFor since php-debug now moved to new api e008e41d27b fixes #113242 0652b9cbec2 debug toolbar docked actions only appear in debug viewlet 750948b7b5c fix hygiene as well dd9e52a40d9 libs a96a29b7535 pkg-config 035e15b7712 revert pool cefe06d375f build-essential 09e281c091a Fix #113732 7449d86b89c use esbuild for minification ded513b20b2 Make cancellation token in resolveTreeItem required Part of #111614 7814ffc41ec Fix #112663 699d736d785 distro 82b99b3286f skip flaky test (#113620) a9ed4e9b0a4 Fix quick input button height Fixes #113745 c4aeb68a68a debt - introduce and adopt flakySuite 174bfe9b528 simplify gitignore 008f8dcdeea missing compilation 503a2458dc4 Merge branch 'joao/build/agentpool' b5ccd30c95d css - less generic rules to prevent leaks f64cf2922f9 Fix remote explorer views getting changed across windows (#113237) c4ea0b55906 fix some spelling mistakes 6659f8dfe58 use compile agent pool fe795313d8e Revert "parallelize hygiene" bdbd644c27d parallelize hygiene a1760b1a6c2 Merge branch 'master' into update-dom 998e5e2ea67 onDidChangePassword -> onDidChange in secrets API 1bb2ae0e365 Allow ResourceCommandResolver.getRightResource() to return undefined (#113364) 7db413d4c10 Move secrets API to extension context a48ef56fbf7 Fix compile for current TS version 3ed300eb9db Add simple browser extension (#109276) 69dfa670ef1 Fix compile error if using older ts version 4d8895c7b72 Just kidding, keep proposed onDidChangeAuthenticationProviders API for now 942c3bad6b8 Sort contribitions b565c422aa3 Add find all references command for JS/TS 0f9ee988467 Remove deprecated parts of proposed auth provider API 13317a96944 Merge pull request #113618 from shskwmt/113318-diff-empty-files 87c2cf1b590 Merge the Monaco Editor job into the hygiene and layering check job 604e2466d90 Merge branch 'master' into 113318-diff-empty-files 48742bd3a10 Update ts grammar pinning tests 3bdf2825d07 parallelize 79fd01a78b4 use yarn task again 74623bc93c5 Update JS/TS grammars 2b040e87633 Unskip tests Fix #113761 714f85fc189 update build pool fee7cdacf72 add provider naming rule 4649e1205f0 fixes #113725 726acc51308 use vscode build agent pool 9d5f04fa0bd #100700 remove the skipped test e4bb3ff7295 fix #113217 414a4c0f511 list added view descriptors in ascending order in the event 8a0058b9e3b Merge pull request #113549 from nrayburn-tech/issue-107028 fc4b40b6338 fix type casts f4ab083c28e update todos ca370bdb0ae update my work query f93a2b62ef0 fix cr issues 00181ad8951 Fixes microsoft/monaco-editor#2292: `model.dispose()` should work just as well as `modelService.destroyModel()` f02af16530c Remove sync property d883d379d11 Merge pull request #113303 from microsoft/joh/toc ceb8da0be04 tabs - disable badge decorations for compact sticky tabs that have fixed width 4f34edadd65 Merge branch 'master' into joh/toc b62ec5c0eeb move shared config key into workbench layer dce7b0cd63b simplify outline model again ea51fb1bddf fix tests b948e42373c :up: distro 64eb716d57e env - fully qualify our own variables c03bc427cf7 some :lipstick: and refactorings 1e5b337c02b move document symbols command to right contrib fd23a8e3280 Have condition of test publishing match the condition for test execution 8986870317a Merge branch 'master' into joh/toc 9b66dc4ad2c debug console: to string of simple values should duplicate value for each count baf15f1041a Allow tabs to wrap to multi-line (#106448) 8ed509056a9 Merge pull request #113710 from mitsuhiko/patch-1 0490bb1e9ae Merge pull request #113716 from noritada/doc-fix 5bf42c9acd8 Merge branch 'master' into joh/toc b6c601b4fc8 Remove duplicate 'the's 8fc5e370f65 Fix a typo in a log message in extHostCommands.ts 98106c48a07 No need to create fake workspace folder for query builder Fix #111348 3283ade7649 Don't use 'expandPatterns' for workspaceContains search Fix #110510 da4784eaa84 Don't miss creating a new default terminal when reconnection is disabled Fix #113564 4e7e21233e3 Have `computeSync` return an array of results 5edb6102629 Extract all MarkdownHover computation to MarkdownHoverParticipant 408539d8fa5 More cleanup 18982d441bb Fix compilation error d3b643c8a48 :lipstick: 923f34a80d1 Extract `MarkdownHoverParticipant` from the editor hover 2f817caf6e7 Move more marker specific logic to markerHoverParticipant 28795976207 Extract `MarkerHoverParticipant` to its own file 765947c205a Extract marker related hover logic to `MarkerHoverParticipant` 7e7775705ee Fixes #113318: Show having no change when diffing two empty files 72420829833 Fixes #112834: Ensure the markdown link provider is registered before invoking `vscode.executeLinkProvider` 262a7157ab5 revert webview preload changes. 56c808a66a9 Emmet flatten DocumentStreamReader (#113602) b84858babef Emmet remove dependency on vscode-html-languageservice (#113599) 94facfcf06a Merge `ModesContentHoverWidget` and `ContentHoverWidget` 3564f180616 Fixes #40926: Inherit visibility from the previous line 95cfa9ede4e Fixes microsoft/monaco-editor#2276: Check pixel ratio after each render 19b5e736fa7 :lipstick: 689fbbd9607 Increase timeouts 5cc0aa28496 Add support for including line feeds at the end of lines (microsoft/monaco-editor#2265) 4a121608e8c Add test for microsoft/monaco-editor#1235 92d4b14e293 Update rust grammar afb6a0c56ef Fix CSP 15561c41399 Emmet polish 15ba9aee0e9 Emmet improve Expand Abbreviation perf (#113558) b5cd082cd47 Emmet refactor toggle comment (#113557) f1ea605a695 Use blob urls instead of data: to maintain current origin e6daf340852 Better simulate real-life CORS in code-web server 58852eaa854 Add a timeout to the editor tests step 5ea96ec9cd7 Fix `_colorMapOverride` usage before initialization 67bff68034c Add `monaco.editor.setColorMap` 433833fbe02 Emmet refactor reflect CSS and update image commands (#113550) a6bb30b41f7 lazier loading of windowsProcessTree c1bf6e0307b no need to update api 05be32f0551 turn on renderer view 7c11754ad4f avoid duplicated text model. 6a7f07f8b61 update height without scrolling the view ebb316a2881 Fix incorrect icon hiding for files in tree view 5dbfe32be31 Allow tree item command to be resolved later Part of #110498 3a70241a0bf Merge pull request #113536 from microsoft/alex/build-compile-no-container 3e945d092c2 Trigger build 321b407de55 No need to use containers for compilation and hygiene jobs 5b75a42575f Cancellation proposal for resolveTreeItem d746f2d2772 Fixes microsoft/monaco-editor#2220 0dc7faff87e Fixes microsoft/monaco-editor#2222 cbea242dcbb Fixes microsoft/monaco-editor#2236: Avoid using `wrapper` as a css class name 7c937a7a383 Fixes microsoft/monaco-editor#2237: Add API to register commands 0439aef7b3c update dom to use newer methods c44b7d25d91 Refactor Emmet merge lines and select item commands (#113516) c7dbab59ff4 Emmet create new html-matcher override instead of using LS (#113508) a1ea4df9596 Empty output view style update and layout change 4b6280aa8ce Skip webkit 9945754a620 Add editor smoke test (ported from `monaco-editor`) 4df5d59e021 force update metadata diff editor for the first time. cd836bb54f6 Fix process explorer styling issues, fixes #113399 3c518e43d39 Update distro f19eb28477e Avoid installing typescript 2ca3e67f32d Rearrange CI jobs 802a36b0970 Add typings validation 4df5991023a Adopt latest setup-node action cead2666633 Align all yarn caching steps 2a8140c5e8a Let debug wait for tasks to get input Fixes #108443 ffef9abe924 Hide auto forwarded port notification 82fe1e7d0df Avoid touching passed in options object (fixes microsoft/monaco-editor#2210) and avoid deep cloning overflowWidgetsDomNode (fixes microsoft/monaco-editor#2184) da8549b7323 Merge pull request #113376 from microsoft/alex/use-builtin-cache-action 4804bbcdff7 Tunnel factory can return undefined (#113232) 1c0efed5a67 Test cached node modules 9c9e1075826 Fix semantic highlight scheduling 413b5d47057 Avoid ts changes b4789ecf849 Fix typo 2b380bf8c34 Support whitespace options d20f8ed37d4 Add style controls 4b3d54ceb32 Avoid changes 7222b357f75 Fix cr issues d26dbae6cdd Adjust request schedule 0c697b2e334 Merge branch 'master' into alex/use-builtin-cache-action ddb88dac9b8 Avoid touching continuous-build-* 4248e3f7e82 Add Download Playwright step 2697a42ee71 add range WIP 2db89c75e6f rename to inline hints 1954e938504 Adjust hint label styles df24d0153d8 Trigger a build to test caching 1b713df9b61 One more try 941b3513c91 fix view state being stored after picking an element ff5245ea45a Enable playwright debugging 0d671ccb2a7 remove unused context keys a747b66c1ec naming: Outline -> DocumentSymbol when it's a symbol, some cleanup 86dd04ecfc4 fix some outline settings so that they are also language overridable fcba2ef5891 simplify filter updates 61965e6bc50 Use fast 7z compression level 674877fc173 properly implement `revealFocusedFromTreeAside` command 5162c8e8563 Add "Loading..." to custom tree hover Fixes #111615 4468307af3a Fix themeIcon + resource in tree view Fixes #113374 ed87c35de9a Show more port cmdline when wide And add pid. Fixes #112796, microsoft/vscode-remote-release#4120 0b1892f877e Start candidate finding later (#113377) 5b9ea8d3780 fix/workaround rendering issues with codicons and quick pick 02b72216d7c Merge branch 'master' into joh/toc ce02917f666 change cell uri fragment format so that opener service doesn't interpret it as line number, also throw error when trying to resolve a cell as notebook, https://github.com/microsoft/vscode/issues/113307 7b6bca0bf59 modes hover must not bread fragments, https://github.com/microsoft/vscode/issues/113307, fyi @sandy081 bbaff3d568b Adopt `Cache@2` for node modules caching 97ae453378e honour outline problems settings in notebook outline 88bafd443d1 Merge branch 'master' into joh/toc d24ab0a201a Improve comments and code style ff8ae0b93ff Avoid changes bf4e2371d4e Avoid generated file a6fd2cc1088 Use utils type converter caaf2a07a94 Merge pull request #113341 from microsoft/alex/node-modules-cache-key 8b7c6622339 also use outline view state for qiuck pick 4140affee2c breadcrumbs picker must restore view state when being dismissed 7cc87833199 trim MD headings syntax from outline element adc8c296ee8 move more things into outlinePane-land 1b8be429cda properly cleanup notebook outline marker b884f75fa6f fix stale breadcrumbs issue cb020e68a04 tweak (reused) rendering for breadcrumbs 4fa97186146 fix and tweak sorting 9c861dc5fd5 update outline as soon as it becomes visible 871f167341c add IOutlineComparator as concept, remove `outline.symbolSortOrder` 68257573641 More tweaks 382a8740a91 Fix that `VSCODE_ARCH` is not taken into account on Windows 40fb699b6f4 style tweaks 63acd85f702 render markers with outline elements 7728010c73b Invoke `mkdir` with `-Force` 05a5209b4c5 Fix candidate port finding (more async, better timing) (#113342) f23ed65688e Compute node modules cache key in JS to avoid globbing a5360f21b81 Merge branch 'master' into joh/toc fe70487f14b :lipstick: d75115ee9c7 breadcrumbs - make sure to dispose current outline when editor changes f39f31d2ca3 protect breadcrumbs widget from rendering bugs c459ca37441 Merge pull request #113332 from microsoft/alex/modules-list 3de96fd2372 Simplify code b7a5fcdc6d5 show a default entry when a cell is empty ea633831d49 workaround https://github.com/microsoft/vscode/issues/113333 07b28e284ba Bust the node modules cache 76c974bfebd Have a clear list of directories where yarn should be executed 1d7fb0e757b notebooks quick pick should contain all elements 698ed864114 fix issues when user state changes 25709313284 make notebooks outline a tree (using folding logic), also fix notebook outline icons 9f5cfc246bf Add dummy support 434f63192e8 Revert "Avoid generated files" 96b889a113b pre calcuate output height. ec3f4088e71 restore view state df64bfe7f7c avoid view state npe f98fabae6ce Merge pull request #113279 from microsoft/alex/remote-websocket 9a7e8372ea6 update distro 102e69c975b Merge pull request #113311 from microsoft/alex/gulp-lazy-load da802ca8909 style polish 98e05820b89 update distro 1ee7a0e015f Merge remote-tracking branch 'origin/master' into alex/gulp-lazy-load 1630db29bf8 indexOf baac11d5f58 Merge pull request #113296 from microsoft/alex/gulp-no-respawn c1eb24b027f Load expensive node modules lazily 11e600ce2c1 remove console.log d14c8de672f event dispatcher for cell layout. 4afc1ee4406 retore awol feature and fix tests with that 52c1cce2597 Add controller 6fa83ab26d9 move deprecated style into IconLabel, move/rename outlineTree to outline 54da36d181b proper disposing of outline objects d7644dc25ad replace IOutline#resource with #outlineKind a54c02aedd4 add setting: outline.symbolSortOrder 515550212b4 move sort order 088b21038e8 properly layout tree 661c741c302 make sure outline is all expanded 277a8262e0e Avoid gulp having to respawn 310a9ff90fa Merge branch 'master' into joh/toc 22c5527f96c fix compilo 9699993f161 use outline service in outline pane cfccae30dbf Fix layer breaker fd8b68457b5 Exit immediately when a cycle is found and running on the build 9c2a1dc473c Don't restore ports that are already detected Part of microsoft/vscode-remote-release#4112 741a568bfd5 Avoid generated files 8476ff1a9de WIP d2ee88ba3fd add config per UX (breadcrumb, tree, quick pick) 1922771f2e8 move delay "after big change" heuristic to new outline model 1478078ee30 Update task services doc link (fixes #112591) (#113170) 445f259e34c Add sorting to npm scripts (#112725) 297567be0c1 move document symbols breadcrumbs logic into its own data source f2fd0ec094e Use the browser's WebSocket for remote connections 17573d22216 move outline view state into its own file ca08df3de54 fix compilo 1844c541870 testing: forgotten push to enable debugging 84903a3f400 revert outputs should trigger output rerender. 64c38324e65 no outputs to render ede4de744d9 vertical alignment 23658cc8753 empty cell outputs placeholder 597ec5f0573 padding polish for mimetype switcher d345d1bd827 mime type switcher 8b23b938a38 split point takes renderOverviewRuler into account cb5f9b5294d Merge remote-tracking branch 'origin/master' into rebornix/output-view-model 6f11d34f677 resolveNotebook API command takes no argument 9ac5131833c Have the `TunnelService` use `IRemoteAgentService.socketFactory` 11f838c144c fix #112747. af92a731fa0 Fix #113250 0e2a0f9013c Prompt when there is an AMD module cycle d63aa232d93 update distro e8bb7a938ee fix #113243. f0376ac1312 Add `permessage-deflate` support 609d547db1f also show cell-file icons in quick pick 7593c56fdd8 use file icon (if applicable) for cells, otherwise use default icons 5ebbd9a77cf re-use css classes for better icon support 19cf2d424e0 render highlights in breadcrumbs picker 68da4f16d67 remove onDidChangeActive-event 358f3909620 simplify model, add keyboard nav support for notebooks 822a45f6072 add outline target and adjust sorting, filtering for document symbols outline accordingly cece1a4fcf4 use identity provider, some cleanup 88c271c3f71 remove ITableOfContentsProvider, remove duplicated code, add quick picks to IOutline 4d47c40dde4 delete old tocProvider 7cc71d65361 Fix broken remote explorer drop down Partial fix for https://github.com/microsoft/vscode/issues/112750 513055e2e0d show a root element where there is outline-breadcrumbs but none are currently selected ebb69237016 reveal vs expand folder vs select folder... c51830e8efa better revealing and previewing 48c2ad6493b Temporary workaround for #112843 add8753c74c Only terminate after 3 EPERM errors dee0c84e7f0 update distro a512c86f10e Revert "fixes #112750" 57e3aba4e16 Have `/build/` compilation and `createAsset.js` be runnable independent of the root `package.json` baadb591bd0 update nb breadcrumbs as you type c6f70a3a019 :lipstick: c2f1a367ef0 Adjust node module path 29f556a724c styles for notebook outline elements ec6087fe180 Merge pull request #112837 from microsoft/alex/node-modules-build2 a8d9a5eedc7 Merge remote-tracking branch 'origin/master' into alex/node-modules-build2 5c1ff8158b7 Avoid lookbehind (not supported in Safari) b71972bbb00 Support open in background for 'open with' f0c758bdcc9 Support open to side from the 'open editor with' quick pick 64888cafe07 Merge remote-tracking branch 'origin/master' into alex/node-modules-build2 09ca712fe7f Fix compilation errors d78c8689d0a Give browser unit tests even more time, the build agents are having a bad day 36d09208f19 Fix whitespace issues from github merge UI 72cf134322e Don't transform viewColumn used when creating extHostWebviewPanel programatically 32d935ebf4f Extract type guard 186792cadbb #27498 restore extension editor webview scroll positions (#85982) fc35c4275f8 fix #112778. 768cd308c62 typeahead: fix invalidation on 2nd char in zsh aa8962f5dcd Fixes #112353 54dc2d786e1 Merge pull request #112384 from myovan/master 25212c95eba Merge remote-tracking branch 'origin/master' into alex/node-modules-build2 563664f3491 Increase macOS browser unit test time because it continuously times out 16c2193e3ff eng: avoid duplicate prelaunch task runs afafd5e8358 Merge remote-tracking branch 'origin/master' into rebornix/output-view-model a207b50937d dispose webview scroll event when switching models. 4fa0f309ab8 webview cover. 9970299016a dispose webviews after switching models. 361877735ce :lipstick: 71a733b4aa0 finally fixed the OutputViewModel leak eb154856a38 fixes #112366 87629065098 :lipstick: 6ad37a351ff cache metadata and output height. 11d5c625d1f fixes #112750 20831dd3df4 Keep only `@types/*` deps in `/build/` and share `terser` (#112718) 8fdb44467c7 update distro 273a5cdf5aa Emmet comment spacing fixup, fixes #112835 a8cadd39129 Merge pull request #112812 from microsoft/alex/node-modules-build b4a22eba377 recompute output height in diff view model. 6ad2dccc786 Emmet Toggle Comment HTML :lipstick: 775bf46bad4 Adjust references to --list-extensions option ea15eb4e5ab Strengthen sameNodes check :muscle: fixes #112829 94fbbb38ae6 tricky selection listening 58985749f9a memory leak e064043f7a8 wire up outline service with breadcrumbs control, WIP 202a8fa3eb0 Merge pull request #110961 from a5hk/snake d5bed1d4a7c fixes #112792 db27c552056 Adopt `ICodeEditor.executeEdits` c2044d680f4 bust the node module cache dc169ce06df Merge remote-tracking branch 'origin/master' into alex/node-modules-build da4192d2879 Leave sorting deps up to yarn 3e8b2d86d17 bring back css integration tests 6d552620316 update vscode-uri c1b988bcf86 Merge remote-tracking branch 'origin/master' into pr/a5hk/110961 2df64d27dd2 Merge pull request #112777 from microsoft/alex/prof-v8-extensions f294e4a9cc4 Merge pull request #112810 from microsoft/alex/node-modules-types ff744b4fa8a fixes #112281 7cd137263a8 Fixes #112382: Assume that control characters are wide ec9ba0edb68 debug: Focus child session instead if it is stopped 02443d02f5a Fixes #112412: update comments to match placeholders b0af35c8104 Merge remote-tracking branch 'origin/master' into alex/node-modules-types 711ec31c7ea Make sure to call done() from unit test 8c5966fc770 Merge remote-tracking branch 'origin/master' into alex/node-modules-build 75fc5cf5258 update distro 14cb2089dc2 update distro 2c4988b7639 Fixes #112301: Wrapped lines contain the wrapping whitespace in the line content 295912f7d50 update distro 4fc14314b69 Fix compilation problem 3bff49d2468 More tweaks to Windows cache exclusions b3e390baa7f Merge pull request #112760 from microsoft/isidorn/workingCopyServiceMulti c78bd5bd783 createFile and createFolder only allow single operation until there are more use cases 60c263ce1ce Merge remote-tracking branch 'origin/master' into alex/node-modules-build 318ff7e94a8 Merge remote-tracking branch 'origin/master' into alex/node-modules-types 8dc8025cd48 comment out css tests on windows 4efb4a2e8db decorations - show them next to the editor label when tabs and breadcrumbs disabled edd37076b4b Adopt new mocha types 7971d627ce8 first version of outline service, outline creators, and implementations for document symbols and notebooks db82bc13aa6 Move /build/ dev dependencies to root (#112718) f74ad9692f6 Align `@types/*` dev dependencies (#112718) 9034b769fa8 fix #112805 0adc15c4bb8 configureCrashReporter should check whether we are on Electron b069dbe0aeb Tab decorations need to update scrollbar (fix #112799) 9c30a0e98cc null guard 780ae767608 workingCopyService: create and createFolder also use IOperation interfaces e588e04b5ef Merge pull request #112803 from thebinarysearchtree/master 34f1e7ae207 Revert 0d14d3e38a8aba6e2bcd6d5dd729c4d47b3d4f97 1585a290afc Merge remote-tracking branch 'origin/master' into pr/thebinarysearchtree/112803 4db298aab63 Avoid text flickering, just render   to have a height eb3cfcda015 chore: bump electron@11.1.0 d4e98289a34 [css] revert changes to test runner 5d6f7a65d38 Use IFileOperationUndoRedoInfo abf082cf74a Log more details when exiting 65c59b509b6 Merge pull request #112798 from microsoft/alex/node-modules-types-keytar 3d9d6b34204 trigger layout when resource labels have rendered, fixes https://github.com/microsoft/vscode/issues/112799 daad75c2c6b [css] update to vscode-uri@3.0 d6a6b44a130 Remove all `yarn` warnings aa2864d53f7 Remove most of the `yarn` license warnings 90f9a7de885 Execute `yarn --ignore-engines` for extensions 1141224b80f Remove debug console log 13ea3e08a1e Fix hover widget. 1324dcf085c fix #110982 63d49f6a135 don't disable the current remote resolve when bisect'ing extensions, fixes https://github.com/microsoft/vscode/issues/112473 2dd359c7153 Use the root typings for keytar since we are loading the root node module (#112718) 2c83509a154 Fixes #112666: Recompute minimap options (which hold the background color) when the tokens color change df53f46ac2e Merge pull request #112670 from chenjigeng/fix/hover-link-encode-unnecessary 98166ea0b19 update distro 0d14d3e38a8 Fixes #112391: (Re)layout the hover widget after adding async content 7f1af9efb1f remove more unused code 5e865477065 only show render style switcher when the property is expanded 141572a2b21 render outputs in text by default. d46abd43537 avoid plain/text being rendered multiple times 71d63c7f741 no transform for ITransformOutput 3337693651f dedup. 8d5b2904fa2 :lipstick: 1c2d88e68ba fewer weird as cast 6d378dbadea no more casting for Single/SidebySide diff view model. 819161c7bca DiffSide enum to replace boolean. 6dd6d4e5138 Add `--prof-v8-extensions` flag (see #112393) 681a3e413d0 :build: c927a8015b9 Merge pull request #112771 from microsoft/alex/node-modules-vscode e48a21c44bd :lipstick: e9abb31537a Reduce height of notebook add new cell toolbar 2292bb0283f Make notebook add new cell toolbar visible on hover/focus 25cb0d70d4a Update removeTag command d03c18661cb Windows CI: Do not cache symbol files fee6dbf4e0e Merge remote-tracking branch 'origin/master' into alex/node-modules-vscode e461782061e Fix compilation problem 5e3dfd3bb8c Merge pull request #112765 from microsoft/alex/node-modules-typescript 7ca71e763d6 Move away from deprecated 'vscode' node module (#112718) c5d42b27722 renderers layout update 88f32df1d71 Merge branch 'mocha-update' e0498f0cdd3 Share typescript node module (#112718) dadb18c39e8 Upgrade Emmet removetag perf + behaviour, fixes #104173 04d74117859 eng: update mocha 2 -> 8 877dad976eb load renderers. bcef72ddd01 hygiene: switch to terrapin 434bbbde983 workingCopyService: take options alongside each argument 710360b7db4 resolveNotebookContentProviders does not take arguments. cb2167fdb22 Output transformers take ICommonNotebookEditor 3551968d69a update distro 2b25e675eb7 Merge pull request #112722 from microsoft/alex/node-modules-webpack 6112cc76f79 Move asClassNameArray/asClassName/asCSSSelector to CSSIcon 03a3e114151 No more spinning of loading progress. Fixes #112711 e42440bd357 Merge remote-tracking branch 'origin/master' into alex/node-modules-webpack 3072f2dd5cc Merge pull request #112719 from microsoft/alex/node-modules-mocha fba51d8422c :lipstick: async-await ebb94b4795e remove unused code 65e1707d19d format js/ts on save 3697925e5bc Use menuService across all views for context menus. Breakpoints. a341f800668 fix https://github.com/microsoft/vscode/issues/112745 d5d6096b65c tweak wording for participants https://github.com/microsoft/vscode/issues/111878 5ff434d97f1 debt - use localized string for bisect title 79d6bf6ca82 allow commands to specific a mnemonic title in additon to "normal" title 6efbb251b5a debt - tool tip never has a non localized variant b34a71e2279 Fix view order in remote explorer Fixes #112200 cbc47b1d7be Do not share `terser` 14440356e52 Merge remote-tracking branch 'origin/master' into alex/node-modules-mocha 438feb87d58 Merge remote-tracking branch 'origin/master' into alex/node-modules-webpack 4fae21b64d3 don't show dialogs when running tests 0e68af72989 fixes #112603 5c386371d8f Merge branch 'joao/build/compile-artifact' d981cf5b81b Revert "Revert "Merge branch 'joao/build/compile-artifact'"" 804c90cc49f Adopt more breaking changes from copy-webpack-plugin 77f4fb49952 Do not upgrade mocha 49962373131 fix tar d07422ec301 rename workspace action for duplicate b7aa0f7d406 fix template usage af42aa5bff8 show diaglog when extensions participate in file operations, have "don't show again" option and command to reset choice, add logging 2aa92c6e83b Adopt copy-webpack-plugin breaking change 3c4de451b48 ux - distinguish folders from workspaces when opening (#77718) 4f4ba928851 debug console: use menuService 29eb3fbc6a1 build: tarball compilation output first 6de06bc8c86 update distro fd85ae4f4a2 Avoid recompiling remote native node modules (#112644) 1cada18542a Share webpack related node modules (#112718) ed19f6082f3 Share mocha related node modules (#112718) 83c47f90d0d debug: move Debug Console action to the `...` menu adf764617e9 remove unused keybinding id e3612f789be Use registerAction2 in markers view bb157721fbb `/build/` and `/test/` should only have `devDependencies` 0929ea86cd3 don't suppress preview when handling onWill-events anymore, https://github.com/microsoft/vscode/issues/111878 a7fbcc27b75 Merge pull request #111222 from microsoft/joh/tabDecorations a09cbd1b118 timer mark sources must not be unique, fixes https://github.com/microsoft/vscode/issues/112708 834488bd7a1 merge last stages into Publish c757f9c70a1 merge macos jobs into same stage c8aaeb75137 Revert "Merge branch 'joao/build/compile-artifact'" 6d683afb84f build: compile smoke tests 2445f698949 fix hygiene 7aee2c7d45e Fixed issue -filtering by extension in Change Language Mode (#112435) d8a7c31aba3 testing: structural and perf improvements cf94178b897 testing: improved test explorer, cancellation 201112e9948 testing: continued work on test explorer 732c73ef796 diffBrowser. 7c4757dd0ce private _ 0055e658c99 style polish d6e8feb7744 Check in additional file for uri opener api change 13770874831 Ignore case while checking pressed keys in webview ecd2325f863 Escape backslashes in keybindings for release notes 423076ab493 Pass schemes to main thread a28b7022e0b Emmet fix edit point commands #112691 ae1077255c1 adjust test for removed process env key 1248ddadb8c fix #112683 bd38c65afc7 Add workspace tagging for java (#111303) 847fd19b026 Merge branch 'joao/build/compile-artifact' 21b1da3fb08 absolute layout for side by side output diffing bd131b85785 dynamic .artifactignore a8b4e9817f5 finish compile artifact 581ae611c2b debug: watch expression use menus 483bd40550d Update several Emmet commands (#112597) 2156931d38b Better side scrolling 41f450129ff fix: hover link encode unnecessarily 9474102e7b2 debug: more transition to commands, get rid of StartAction bffa4045489 Have TentativeBoundary trigger rollback (#112510) 9f27d99af01 Update isVSO check with new remote authority af7edd782e1 move progress and cancellation of file operation participants into its only customer so that progress stop when applying a workspace edit and showing its preview 6a342fe0a47 update original webview based on original text model. cee8cbd9f29 fix cyclic dependency b8ddffe7b41 rename setting fc6bf56844a Fix executeTask for composite tasks Fixes #112545 e5e25a027e3 Update src/vs/workbench/browser/parts/editor/media/titlecontrol.css c78bc564604 update references viewlet 3776ba6be2f Fix activity badge on ports view not going away 7d0d3835e6e cleanup hygien fb2a5e18bb2 fix build df0eda5adbb build 24a98f06442 debug viewlet: use registerAction2 and restructure the whole debug toolbar cda701edbd7 debug: move colors to debugColors.ts from debugToolBar d14fb9da0fb fix build 6236a5bf8b5 compile: use artifacts instead of cache ad362089bea add proposal of CancellationError, https://github.com/microsoft/vscode/issues/93686 b4b8bcda479 tweak padding-right for decorations d5a632e6fcf empty 0cee1531f79 Merge branch 'joao/build/remove-postinstall' 7db8e3b08c0 fix https://github.com/microsoft/vscode/issues/112418 ce1ffe0e7f8 link webkit issue for missing timeOrigin support a780c7d4515 spell out GDPR types instead of using mapped types 018b924fee2 add logging for #112649 3b5cdf3a0eb backup - move progress reporting into place where save actually happens c7d468d8ee3 empty 6260e655bc6 codicons -> iconLabels a1b6de93363 Fix markdown span style filter Fixes #112606 1abefb9be2a Fixes #112652: A disposed IPC channel should reject all requests with a canceled error, not resolve them 39bd9df739f Show a clear dialog on web when the remote connection fails aaf73920aea Ensure task executions get cleaned up Fixes #112247 6f08397de0f Enable fileWorkspaceFolder variable for remote tasks Fixes #112514 78fc9abd92e explain why ::after "inherits" italic rendering 21c8c9f768e Merge branch 'master' into joh/tabDecorations f75e0388adc remove passing user target 8fbe27ae9aa Do not touch the perf marks names ec2d8d67725 Try to give a good stack trace in case loading code fails ac7eb534b82 Increase web worker extension host timeout from 10s to 60s 1100f276812 fix build 77221e5f604 Allow strings as host name for port forwarding af519ea93f7 Merge branch 'joao/build/merge-distro-directly' 9fe0d3c372f Merge branch 'joao/build/child-concurrency' 2f8dee4d87c Add logging for calls of `process.exit` in the extension host 20920160792 don't show loader stats in perf view editor anymore 25d6642db52 debug: preserve focus in editor when integrated terminal is shown so match debug console behavior and to make sense for accessibility d833f8bc10d quick pick - use Alt as modifier to open to side 3229991032c :lipstick: 083d38475c9 Tab bar not rendered on first load of only a welcome editor (fix #112618) ede8d447025 Bump distro 8a2ec350bd9 Restore terminal UI state and layout when reconnecting to remote terminals Fix microsoft/vscode#109244 fd6debbde2b hide inset and update layout for the left webview. 1741bbf7e6f render deleted output 2118388b0e1 two webviews fec4672c27a output diff container css class clearing 09d72bfaec8 :guard: d2aebcd2c58 absolute positioned borders elements f91b8f2e7ae More scrolling in getting started 01b04218586 Take ownership of workbench-welcome 03081f5a3a8 Polish skip location 6d3c2e68355 Adding proposed external uri opener API 542670762fd Make sure we dispose of webview revivers when the main thread extension host is disposed of 03c528450f3 Small formatting cleanup bd8e81d122e Merge remote-tracking branch 'origin/master' into rebornix/output-view-model f6a796b6693 grouping types in notebookBrowser 86838823e3b move genericTypes into notebookBrowser 706b8ddf983 single backlayerwebview! 05f69f7bbd3 share the logic for renderers in webview 64f7c1b0444 :lipstick: b56da5e92aa align backlayer 1 and 2 c7616704115 fix build errors fa464a731cc :guard: 9694d9f5c96 move output layout update logic out of back layer webview. e2458c2d705 Fixes #112483: Use `JSON.parse` and fall back to the tolerant parser only to extract better errors 9c70421d476 IDisplayOutputLayoutUpdateRequest. a7cac806230 no need to pass in cell for updateViewScrollTop. it only talks about output and offsets. e3cf33470d6 link clicking in the output webview 65bc4153aed support scroll position syncing. 15ba91d098c Massage mark name to align fbe4aa28ff4 Collect and deliver perf marks from extension hosts (#112552) 691a2ce4ecd add timeOrigin as implict mark whenever possible 976b9b5cda2 :lipstick: 0ff71d32138 update output in webview position when list view layout change. c782b708b18 cell body height to 0 so I can click into the webview adaa7d6d638 Update codicons: use new loading icon and animation (fixes #112593) https://github.com/microsoft/vscode-codicons/commit/ca2658d7973430426109b97f9f57c9f50b4f1717 7284389d4bc Update Codicons: Make circle-large-outline 1px outline (fixes #112310) https://github.com/microsoft/vscode-codicons/commit/7a530b493aadaaadee5ae62e33a90235795dfc09 bd67f0d0cab :lipstick: ff02224c1bc move output layout info into genericCellViewModel shared by notebook and diff. 179adc5f7fd support removing a setting from all targets 284ace0bb6f Cleqn up codicons 35be51e43d9 open editors: adopt registerAction2 bbb797d3e4d clear static outputs when collapse outputs panel caa595fc066 absolute position of mixed static and dynamic webview 3080c3e88c4 webview output first load f158da4d7c4 Fix microsoft/vscode-remote-release/issues/1801 063ecfe0768 explorer: adopt registerAction2 66bf5744fa5 :up: distro 5b33fdd6507 API: finalize status bar backgroundColor (#110214) 2daa49098b1 Catch all localhost duplicates when forwarding a port Part of #112571 41628e33267 merge distro in a single command dfeaeb5740a debug: check if languageIds is there e2f6cc5a965 breakpoints: use registerAction2 d79f4e7b887 add test timeouts 208380cf766 Dispose a permanent failed connection and fail any further requests through a disposed `ChannelClient` (#112278 , #112568) 834b2b1570f bump cache salt b7d648a8739 remove CHILD_CONCURRENCY=1 for all except windows 9293efd7e7d build: remove postinstall script invocation be2d9834342 debug console: use registerAction2 c97d5e10337 build: use PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD 5ae66f37a89 build: use ELECTRON_SKIP_BINARY_DOWNLOAD e135eee1f1d add perf mark when file systems are registered, fyi @bpasero d782f11c7b2 do not restore theme if setting has changed 6aa034b1ca4 fix build 43c613c7009 timer mark event should also include unix timestamp 02bf5807169 remove migrating sync settings 3a8b161dcc8 #103869 remove user value if set to default and target is not passed 181d5dafba6 editors - show progress when waiting for auto save to complete to take long (fix #112278) 175c1298b84 remove all usage of concurrently 7743d971b10 concurrently -> run-run-all d444826441a Revert "debug: stop supporting enableBreakpointsFor" 6c5916cafe1 web api - document some methods I forgot 607a0b8f133 in addition to file watching, use text file service onDidSave, fixes https://github.com/microsoft/vscode/issues/112477 0716eb086a6 Merge pull request #112306 from microsoft/aeschli/remoteIcons 0f56b9b6868 add embedder API to retrieve (startup) performance marks 6eb9b8938fc debug watch: use registerAction2 092b1365c76 variables view: use registerAction2 8ddbdd338a1 Merge branch 'master' into joh/tabDecorations 690e9f1e02f add ITimerService#whenReady bee1e648979 Fix Configure Task for workspace w/ 0 folders Fixes #111815 aa587fe40a3 Catch bad progress location in extension trees Fixes #112211 b54f979ccbb Merge branch 'master' into aeschli/remoteIcons 05b94398d5a update distro 27cefc2a818 Rename `Protocol.onClose` to `Protocol.onDidDispose` 9cc0fb57434 windows - add helper method to know used window 460b6e720ef in browsers perf util is just a wrapper around native performance 1537b5bcc25 windows - cleanup hack for empty windows when restoring afbd2e826c7 Remove Build jobs for now c42e44e756b :lipstick: settings descriptions referencing other settings a1f815a4c9a add option for preview navigation behavior (#112389) f3e95ede9e4 Remove unused import 29e3f670384 config - careful with undefined/null config props 6b950b3d685 :lipstick: d36f19d1d3d quick pick - configure openEditorPinned properly f9bb8e62001 fix #112467 5114f5d76a1 Merge remote-tracking branch 'origin/master' into rebornix/output-view-model 89fd7be7fbd test 9ca7b4ed4df DiffElement cbf9234d4fd hook modified webview a9c5d7cc3c6 chore: fix CXX export 6361cfab60c Add scrolling to getting started page (#112500) b2c0291c254 expose notebook text diff editor list row container cf242ccaa83 prep for webview renderers ca4601deba4 no unnecessary as any f656636cc38 :lipstick: 14b7df48c39 do not restore default theme dcda9032e78 output height calculation in view model by reading folding state. 239c211326d positioning for dynamic height output 4d0195152fa Add a slice to fix compilation f76b1c25e7e remove quotes since we don't spawn shell 79a42e72217 use menu registry for sidebar actions 9565cf54c63 chore: bump electron@11.0.5 b44750d551d use menu registry for views visibility actions 387f4f4b112 Skip electron binary download 7989b98326e separate side by side and single side viewmodel 428008f3a56 Align versions across `package.json` files d6e0a5424f2 Use directly `yarn compile` 3ba967fd7eb ext: log errors running contributed commands to the console for debugger vis d583e26cd09 fix show view submenu in debug 0f33afa9f96 fix #112217 by replacing look behinds 42e31e92e29 cursor and user select update for output 5431ddb506f Bump distro 4263025dcdd Merge pull request #112279 from chenjigeng/feat/builtin-extension-report-issue 287137afbcf fixes #112415 8fca5328ac5 Merge branch 'roblou/shellEnvVar' dce22cf74b5 Fall back on node API when $SHELL is not set, for user shell and shell environment discovery See github/codespaces#1639 8fbaf86e4b1 fixes #112413 3ee9eb9be4a fixes #112417 a22b5d54ad4 [json/css/html] adopt lsp 316 c2a8940127b simpler perf mark for didLoadExtensions 17c777b1119 update distro e2ba0809426 fix typo per #112437 29edd0d2a90 Use panel title context menu for contextMenu actions d766e6baa8d =revert changes to themes.contribution.ts 6d8f5bf363a Merge remote-tracking branch 'origin/master' f7d9fe9b720 no trigger specification 8876c6debba enable master ci for production build 5b2015c9a00 telemetry for raw timer marks 72e82ba7b25 Make simple file dialog more predictable for screen readers Fixes #109722 9ac0fc87902 remove unused/duplicated perf.mark a6f2524e4a8 - introduce panel title menu id - move view container menu acitons 43017777275 Update grammars c87c95a1a04 prefix our performance marks with `code/`, e.g `code/didStartRenderer` etc 7381f53fd18 windows - move out more state code into separate class f9b89066c7a remove dependencies stage 5f31a6ddc66 Revert "fix yaml" 064e1d19dfd fix yaml 2f2004f5fa8 faster dependencies check d67e33f0b0d split compile & hygiene 2af62a03fe3 simplify continuous build b1524d6b34e build: VSCODE_PUBLISH 200d60cc13c Do not create issue on failure c7df7d91f42 Move actual gulpfile logic to `/build/` ac6cadf8ee2 Fix gulpfile e56597ab7a6 Save 1.5 seconds from `gulp` startup time 5f20ce95815 Increase yarn's network timeout 7d4c5a2c0fb Add a dedicated `Build: macOS Node Modules` job 90f0cf5b313 Force cacheNodeModules to fail 28776ec1a28 Cache yarn cache directory 704393a57cf fixes #112410 7168a1bf157 remove unused measure `ellapsedTimersToTimersComputed` 8b5984e7b83 Windows state deprecations. Fixes #112443 4df387f5ad1 add a way to read marks with source, adopt in perf view editor c666c1d72f2 Save remote Help&Feedback enablement globally Fixes microsoft/vscode-remote-release#4165 3aa342ff68e Remove use of `getActions` from tunnelView.ts Part of #92038 4ba273fddaa name marks with source when importing them 51a3943093b Delete PR workflow stub 814bf82b853 Split compilation job into core compilation and extensions compilation 672216afdab Fix auto-generated link c7319db91ce Remove use of getActions in remote.ts 16e715dc38e Bump ini from 1.3.4 to 1.3.7 (#112276) 45c7cae914d Bump ini from 1.3.5 to 1.3.7 in /extensions/markdown-language-features (#112275) d1fff1a0836 Check in `.js` files from the `/build/` folder to improve build speeds ec5da6d09c1 remove direct writes to `globalThis.MonacoPerformanceMarks`, use native performance instead, import native performance entries into timer service, fyi @bpasero 4b88174158f notebook: use icon names in registerThemingParticipant d55a3f0bcaf Merge pull request #112296 from yeswolf/master 71aa067bbd3 Fix intentional failing test e25373f16ea One last try at automatic issue creation eeded3b8c45 fix #112447 614e183e9a2 Attempt to avoid skipping steps on failure cb84c32f5ac fix #112448 db4264eb3f8 Fix variable b7f7b9fb8bd Intentionally fail to test issue creation a861e22c7cc Switch to using `github.sha` b742a4c8ea1 Create issue on test failure 707afc26d6f Skip compilation if the cache is hit 36b48f49849 milestone update f413b81fcda windows - move state handling into own class 75eeac01049 Split Linux Build, add macOS jobs 6f08741eb91 separate viewPane and viewPaneContainer a4206d19b0d rework perf-util, only allow for marks, separate perf-marks from renderer and main process (and future other processes) 24b18f1bd1f add perfEntries2 75f3a503fc1 create and dispose menu for context menu actions 18ded3c91b2 windows - shuffle state interfaces over 9489147837e Re-enable build jobs 51f18c087dd windows main service :lipstick: d8a48910053 windows - first cut cleanup of windows finding logic 1a1db8f00e7 bootstrap - add more typing info 1a2132f16ee web - show dispose dialog only upon user interaction 28964872e7b lifecycle - log long running operations preventing shutdown (for #112278) 87b4380045d Merge branch 'master' into master 96b426ef1da Remove dupe context key service registration in cell Fix #111280 873f23dc4cf Fix line highlight when navigating to search results Fix #106209 7e16a1e72ed Fixed issue with dragging to select text on Hover should not hide it fad4717a787 Fix issue with dragging to select text on Hover should not hide it 5954c8366d9 Fix #112267 - Revert listener inside Submenu action - Create listner for submenu action changes in view menu actions ced21a0abcc :lipstick: fd44d15c223 fix #112337. fe4f9a9e6fb testing: add to i18n 91bb3857ac9 Use tree widget in the process explorer, fixes #104013 75dd72eeff5 move layout info into diff cell view model. a9bf16e0019 testing: base test explorer f30948328de update distro 70a67a1b844 Convert to use `for ... of` c5c6c824113 Add Windows 7ae944e4b04 load static renderers 70355b66765 Add macOS 374e7487bd2 Run unit tests concurrently 58e88100755 Try `yarn concurrently` 73c050511b5 Avoid respawning ba16f865b22 Remove compile and linux build for now 44b918b6248 Avoid "&" 0059c4cbfe0 Tweak combined step 072d922c450 Revert "Revert "Pick up offical TS build for web server"" 5b8a6ee21b4 Add more logging to resource loading for webviews 771068e9978 Remove unused function 3bad41ff3c3 Attempt to run things in parallel 0dd9cdbb45f Fix env variable usages e2e23aeb777 Add a compile and a linux build step f79e3aea635 Fix notebook status bar icon colors (fixes #112323) e3e3f8802ff Abandon composite actions since they cannot actually compose on other actions d1d4143971a Attempt using absolute path 7e7c1de5697 Follow the expected naming scheme 623440c55c1 Extract cached yarn logic to a separate file 01ea0ecc098 Remove playwright caching eb5c4388c74 Revert "Pick up offical TS build for web server" 92723a8a2c8 Bump markdown-it and highlight versions 52d2132e8d9 Move valid layers check together with the hygiene check 4942f242050 quick fix for setting description typo (#112327) 641abd4170d Extract the hygiene check to its own job 0ec1755aa9b Simplify caching strategy d819caf1d27 Fix if condition c7fa4ef0218 Troubleshoot expression b2444f539bc Open Search Editor Action Bar item should clone search view's config Closes #112209 e1bc069d6ab Push workflow stub file e8922b83b11 Update arial-label when renaming a terminal. Fixes #99072 baacaaca3e1 feat: use baseUrl first fd45ba86b05 chore: remove git suffix c197be4ef74 externalize remote icons 76436a4d435 Cache the /build/ folder compilation 2554c8c14e0 Remove the `linux-node-modules` job a0122361111 Define `path` for the Cache node modules step dc99b1ab774 Add the key property to the Cache node modules action 5e6152dcb39 Introduce a `linux-node-modules` job 50d0d5f0d25 Theme icon modifiers should work everywhere. Fixes #112298 2fc36a7d386 enum all breakpoint icons, avoid css selectors with patterns 824df4da78f Fix duplicate name 00c3a0bc9dc Gotta love yaml b95feff0398 Fix indentation 27dee688674 Attempt to revive linux github action b9b1b6156f9 Run compile on the `/build/` folder if the node_modules cache is hit ed584f486eb use tt policy for nested workers, https://github.com/microsoft/vscode/issues/108400 d39671e7cb1 Convert to use `for ... of` 6d79a70a362 Add more frequently-used Ruby filenames to Ruby bundle (based on the actual TextMate bundle) 8f78f153fe0 Fix #112221 bfaaca632f3 Fix array syntax d9f2b49323c Attempt to cache all node_modules folders 8af387dddd7 Fix #112287 b857599fb9b Attempt to cache root node modules 887bc527439 remove extra separator 684ceea0ed6 :up: distro c350d8b4238 #92038 Move panel actions to use menu registry f211a2bbe96 clarify docs, fixes https://github.com/microsoft/vscode/issues/111686 fff6f7414f0 :lipstick: remove commented code eea0681288d adopt `actions/setup-node@v2-beta` af74fd420bf nuke getSecondaryActions from outline, https://github.com/microsoft/vscode/issues/92038 56e35cf038f #92038 Create view and viewPaneContainer util actions 136df0d897d nuke getActions in outline pane, https://github.com/microsoft/vscode/issues/92038 d94ba914134 #92038 dispose actions on change 84e5cdec293 #92038 Use menu registry a029dda2fe7 move suppress logic, https://github.com/microsoft/vscode/issues/111878 0a2cb7630f3 apply workspace edit from onWill-handler from within renderer, https://github.com/microsoft/vscode/issues/111878 61af2b26daa feat: support Report Issue capability for built-in module 1d5611c9d8f No need to install anything f201645d8f7 Do not configure `xvfb` 019b5ab4c99 Remove yarn `CHILD_CONCURRENCY` limit 124b4bd151d Try newer `actions/checkout@v2` 3b34ea9f163 Update commands.ts (#112222) 32cc71c4a73 web - do not ask for clipboard access in ext tests (fix #112264) bb68097d15f add extra check to understand https://github.com/microsoft/vscode/issues/112263 fdbd7bf6bbc web - tweak dispose dialog 86deb14fd3c make slow timeout event slower, fixes https://github.com/microsoft/vscode/issues/112262 6a03e96bfe3 Merge pull request #112206 from engelsdamien/master 69edf83c14d Merge pull request #112224 from microsoft/joh/worker 38a89809051 Avoids triggering autofetch unless setting changes 83f43bee1d5 Changing autofetch to a string config which has "current", "all" and (#111090) 39f78228fa6 Add missing file to commit 4d4f3a305c2 Observe the confirmBeforeClose setting for webviews 08e6047a050 Remove extra lines 0370a6ebf5f add explicit jsdoc types 618bf22ca5e remove !. ed0634933e2 :lipstick: 29bc5baadc9 Only prompt if input (#112113) 4586eb012f9 renders all deal with output view model. a416c1534bb #92038 Use menu to register actions - Extensions View 996c3495b49 #92038 use menu registry to provide view actions f46cb57055d move picked mimetype to its view model. a5f84617e22 chore: fix run-on values for snap build (#112248) 6424b09b2c8 Fixes #111909 - adds user agent to push 42cdda5ab0f Erase types to avoid using lib.dom.ts types from `/common/` 92adef0bac1 Fixes #108822: Do not render the minimap shadow on Safari c503386ee3c refs #111463 41c6b79537b [GettingStarted] Slow down animation when expanding/collapsing items on sublists 9823eb0bf34 Avoid endless loop (fixes #110392) d8fce51aed8 Tweak getting started 354d5ff8cd4 Tweak get started detail pane (#111840) 50ada457c55 change name back 308e723e181 yet another tweak to startup timer telemetry de2c02eb504 use css variables for code lens font, https://github.com/microsoft/vscode-internalbacklog/issues/1674 10e40063d9d Delete is no longer irreverisible ed04ea29d3a print nested worker errors onto console 1fb86647518 support nested worker when running without iframe 4dce67232d7 add failing Worker ctor inside nested polyfill worker 32ba217a5b0 support https-iframe b3d95fea2ab web - inform user if workbench gets shutdown 91f00c7c285 web - allow to retry clipboard access (#112089) 61995eab4aa fix linux deb repo pointer e6d238e251a web - clipboard warning when failing access 86a23116059 Set tunnel information for embedders Fixes #112213 e5664f1678e restore and update CSP header 49b84b4bd97 :lipstick: d90179bef2f some simplifications b44f2b3db2b Merge pull request #112144 from microsoft/isidorn/replFontFamily 4a76840f7f1 fix setting --vscode-relp-font-family variable caf0943a994 Merge branch 'master' into joh/worker bc95e893efa web - fix window open 90f2d386c82 Updates tsec to latest version using TS4.1.2 6c9dab1259f web - register external opener to prevent unload on expected href changes 039f15a0f93 :lipstick: c7fa31fc926 [bot] make *duplicates link to a query for issues with matching tags. Closes #111903. Closes #49912. 5fe4f5583cb fix yarn.lock leftover d2965c18bf6 Prompt web users when they try using ctrl+w/cmd+w when focused on a webview 04ec120e2c3 Exclude dist from eslint and hygine ee64fdae8b7 Use dispose directly instead of loop c3d8989b24e Enable going to stdlib on serverless 582f8f6bb0a Bump Static version of web TS version e6a13fb134b Remove work around for TS not supporting paths on its own 71fad5aa411 Enable preferConst in TS project 16334048831 Remove unused types 45283df1fda Merge pull request #112176 from microsoft/rebornix/nb-diff-perf f29a3cabc47 Pick up offical TS build for web server 79557ebbd2f Make sure markdown preview is updated if on disk file is updated 6ad6905f981 Re-enable *.integrationTest on windows 542fa93eae7 Pick up new TS version for building VS Code 864e80dc480 fix #112178. 4fe04b10a4c Assign rzhao271 to emmet tasks 1f528e7de40 :lipstick: 2c9bcb8b14c Merge remote-tracking branch 'origin/master' into rebornix/nb-diff-perf 9ccbebed752 revert change to grooming notebook a33e1d617e8 Revert "Fixes #104004: Only run tests if the tests belong to a known extension" f9134083e5d delay cell text model disposing. 4fefd1030ac limit editor contribs in notebook diff view daca95ea065 fix: disable shm usage in container builds (#111787) 31d11a37299 fix memory leak 180506d8844 feat: support open local file b36ec60ac4e notebook: adopt trusted types for renderer scripts b1e7f915d49 notebook: adopt trusted types for renderer 90724cd823a Use `trustedTypes` where using `new Worker()` (#108400) f9d6df8ea1e Fix port forwarded nofication showing for the wrong port Fixes #112159 83993fb92c5 Fix run npm script in folder command Fixes #112152 52cc4e6a0d5 #108793 check casing and use proper extUri 8b9b8df0247 explorer: smarter handling of file events ea7bed08f1b debt - escape quotes 9ce3f9462d2 disable proposed API checks on top-level getters 1aa76d792f6 repl: improve height estimate b8101e698c2 Fixes #106581: Do not react to Win/Super key for mouse wheel zoom on Linux 7951e1f9138 :up: web dev e24db417b2e update my-work notebook 2c74aeb5b6b fix #112146 eec97b824f0 Use the future scroll position to compute the scroll top and reuse the current smooth scrolling animation (#104144, #107704, #104284) 5df492ff594 up references-viewlet db2d6820b19 introduce vscode-repl-font-family a0cbecb188c Merge pull request #112069 from engelsdamien/master c6f4565a203 notebook.selectedCellBorder: fix typo e6fc328247b update iconRegistry doc generation 785097e4618 Merge pull request #111644 from chenjigeng/master 49dc593a83e Merge branch 'master' into master b3c053d44b2 Preserve whitespace in tree hover Part of #112124 59ba6494b9e Better timing for multiple lock files warning Fixes #111635 6643750275a Fix #108647 bdb0ef4d489 Fix #108793 5ee430bf26d Deduce secondary host from the request if possible aef539adfca Remove unused import 'UriIdentityService'. (#112112) bdd890380ef Fix #112121. 82974e8cda6 exthost: use marker to avoid duplicating written log messages 7491d391907 Merge remote-tracking branch 'origin/master' into connor4312/native-exthost-log d66e65fcdd9 fix: Param helper hover getting cut off at bottom (#112019) 3c9a4554702 no longer render overview ruler. f86fd13b2c6 fix #110357. c643c433e82 Remove extra whitespace in dom.ts 759375429eb refs #110392 1cc1f166170 Use uriIdentityService to compare uris, fixes #107779 9ee7e1e87a7 Fix #111871, 'openReporter' should resolve 353227400a9 fix: Debug Console Linker automatically decodes link 71ba241fb01 Merge pull request #111897 from microsoft/alex/111128 2ef8227aacd Log extensions query telemetry data 838b3949ec8 Add workaround for #101754 ad10dc75901 Improve `writeFileIfDifferent` logic 88f09bd5f25 Move `getElectronAcceleratorForKeyBinding` to `WindowsKeyboardMapper` (#101754) 2add9e300c7 Uncomment test 3a47fc387a5 bug fix, see #112013 (#112015) b2eedd8ee09 Add `wss:` / `ws:` to the CSP affd21e65ac tabs - partially take changes from PR 106448 to reduce diff b3a3dc9c0a5 Debt: Test findPorts (#112092) 5e5ae15b22c debug: show the hover not so eagarly (do it twice as slow as editor hover) 768bcf45422 Fixed tab switching too fast when wheeling/scrolling (#112034) c8b592a57a3 debug editor decorations: always decorate top stack frame, and decorate focused stack frame if it is not the top stack frame 92d6f00d6e2 debug console: color the debug group elements 9edc69706f8 Just set textContent for custom hover Fixes #111643 46ee31fce0f Try to fix markdown tooltip for Safari #111756 208bfc9970c fixes #112046 66f5e9294e3 path lib usage :lipstick: 08e29d24b4b Remove duplicate (case sensitive) recent workspace/folder names on the welcome screen (fix #111954) bb480d2c4f6 Merge pull request #111916 from microsoft/alex/104004 861e7bcc1b2 Wait for the remote configuration before creating remote terminal processes 0e885aaf70c retry all cosmosdb ops 3212ddbc76f multibyteAwareBtoa - add commented out failing test for #112013 a6946159d4e :lipstick: layers checker 6f5448afac5 Include candidate pull requests 5865aeaa7b9 Rewrites Trusted Types sink assignements 2c937725db0 Updates tsec aa18ca68276 Unable to find the registered languages while saving the file. (fix #111788) 91c7834e926 debug: update js-debug f79bb79d2b6 Suggest a different description (#112049) 3655a82edcb add log to createAsset retry 3136ecb1d31 Do not ignore errors from $spawnExtHostProcess ab8c8dbd8e5 Fix #112030 cf8ed37206e fixes #111850 c2de3a602e8 Tweak candidates notebook query 4250e343e0f use proper repos for linux arm d0475711090 Prompt to save untitled file before run/debug de22e951f9e more cleanup for #111177 ffdc1096fa5 Windows - Taskbar entry context menu is empty (fix #111177) 36628d73b91 add exponential backoff 66aab34216f retry createAsset sproc due to ECONNRESET c4d77ea6193 #89559 Set logsPath in window configuration 3835563e123 Fix #112012 64f63a3dd70 Fix #108266 ea0b7fd29af Revert "build: create asset should still try to add asset" 267188e90de highlightModifiedTabs and pinned tabs issue (fix #111641) 04b9a571b84 fix #111898 on master 878bf135043 debt - shuffle some electron-main code to better layers 25cbe382515 proxy - rename proxy2 to proxy 428d5b1d302 proxy - remove old proxy auth dialog f5da8e346fc :lipsticky: async fc9ff5d569b ci: avoid overwriting CC variables for arm arch eeed0fd5692 update distro eb189c703c7 Fix #111946 86d779284b6 ci: fix condition for linux builds 7a7d27397a3 chore: fix cache condition for native modules b0d4a08e3e0 chore: bump distro 94142bd7e0d chore: bump electron@11.0.3 (#111931) 962bedc4f47 Update version to 1.53.0 423bdb2e263 Fixes #104004: Only run tests if the tests belong to a known extension 707ca0a06c6 nested worker in worker b8aa4141d28 Fixes #111128: Do not touch current line's indentation when pressing Enter c8e490e5e68 chore: optimized code 3e22e6f4120 feat: add the rename test cases of html-language-features 8f712866cf3 fix: check canRename before findRenameLocations 81610ca5b61 bootstrap tweaks cc502ffc10b Merge branch 'master' into joh/worker 5458e760b98 wip 94b2772f8c2 feat: add rename symbol within `; diff --git a/extensions/emmet/src/test/toggleComment.test.ts b/extensions/emmet/src/test/toggleComment.test.ts index 4793c3ed8..42a867f9c 100644 --- a/extensions/emmet/src/test/toggleComment.test.ts +++ b/extensions/emmet/src/test/toggleComment.test.ts @@ -26,7 +26,7 @@ suite('Tests for Toggle Comment action from Emmet (HTML)', () => {
  • Bye
    • - +
    • Another Node
    @@ -47,24 +47,24 @@ suite('Tests for Toggle Comment action from Emmet (HTML)', () => { const expectedContents = `
      -
    • - - +
    • + +
    - + -->
    `; @@ -89,24 +89,24 @@ suite('Tests for Toggle Comment action from Emmet (HTML)', () => { const expectedContents = `
      -
    • - +
    • +
    • Bye
    - + -->
    `; @@ -130,19 +130,19 @@ suite('Tests for Toggle Comment action from Emmet (HTML)', () => { const expectedContents = `
      - +
    • Bye
      - +
    • Another Node
    --> + -->
    `; return withRandomFileEditor(contents, 'html', (editor, doc) => { @@ -252,16 +252,16 @@ suite('Tests for Toggle Comment action from Emmet (HTML)', () => { const templateContents = ` `; const expectedContents = ` `; @@ -298,13 +298,13 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { test('toggle comment with multiple cursors, but no selection (CSS)', () => { const expectedContents = ` .one { - /*margin: 10px;*/ + /* margin: 10px; */ padding: 10px; } - /*.two { + /* .two { height: 42px; display: none; - }*/ + } */ .three { width: 42px; }`; @@ -327,13 +327,13 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { test('toggle comment with multiple cursors and whole node selected (CSS)', () => { const expectedContents = ` .one { - /*margin: 10px;*/ - /*padding: 10px;*/ + /* margin: 10px; */ + /* padding: 10px; */ } - /*.two { + /* .two { height: 42px; display: none; - }*/ + } */ .three { width: 42px; }`; @@ -359,16 +359,16 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { test('toggle comment when multiple nodes of same parent are completely under single selection (CSS)', () => { const expectedContents = ` .one { -/* margin: 10px; - padding: 10px;*/ +/* margin: 10px; + padding: 10px; */ } - /*.two { + /* .two { height: 42px; display: none; } .three { width: 42px; - }*/`; + } */`; return withRandomFileEditor(contents, 'css', (editor, doc) => { editor.selections = [ new Selection(2, 0, 3, 16), // 2 properties completely under a single selection along with whitespace @@ -389,10 +389,10 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { const expectedContents = ` .one { margin: 10px; - /*padding: 10px; + /* padding: 10px; } .two { - height: 42px;*/ + height: 42px; */ display: none; } .three { @@ -417,10 +417,10 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { const expectedContents = ` .one { margin: 10px; - /*padding: 10px; + /* padding: 10px; } .two { - height: 42px;*/ + height: 42px; */ display: none; } .three { @@ -445,10 +445,10 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { const expectedContents = ` .one { margin: 10px; - /*padding: 10px; + /* padding: 10px; } .two { - height: 42px;*/ + height: 42px; */ display: none; } .three { @@ -473,10 +473,10 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { const expectedContents = ` .one { margin: 10px; - /*padding: 10px; + /* padding: 10px; } .two { - height: 42px;*/ + height: 42px; */ display: none; } .three { @@ -500,16 +500,16 @@ suite('Tests for Toggle Comment action from Emmet (CSS)', () => { test('toggle comment when multiple nodes of same parent are partially under single selection (CSS)', () => { const expectedContents = ` .one { - /*margin: 10px; - padding: 10px;*/ + /* margin: 10px; + padding: 10px; */ } - /*.two { + /* .two { height: 42px; display: none; } .three { width: 42px; -*/ }`; + */ }`; return withRandomFileEditor(contents, 'css', (editor, doc) => { editor.selections = [ new Selection(2, 7, 3, 10), // 2 properties partially under a single selection @@ -549,14 +549,14 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { test('toggle comment with multiple cursors selecting nested nodes (SCSS)', () => { const expectedContents = ` .one { - /*height: 42px;*/ + /* height: 42px; */ - /*.two { + /* .two { width: 42px; - }*/ + } */ .three { - /*padding: 10px;*/ + /* padding: 10px; */ } }`; return withRandomFileEditor(contents, 'css', (editor, doc) => { @@ -578,7 +578,7 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { }); test('toggle comment with multiple cursors selecting several nested nodes (SCSS)', () => { const expectedContents = ` - /*.one { + /* .one { height: 42px; .two { @@ -588,7 +588,7 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { .three { padding: 10px; } - }*/`; + } */`; return withRandomFileEditor(contents, 'css', (editor, doc) => { editor.selections = [ new Selection(1, 3, 1, 3), // cursor in the outside rule. And several cursors inside: @@ -611,14 +611,14 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { test('toggle comment with multiple cursors, but no selection (SCSS)', () => { const expectedContents = ` .one { - /*height: 42px;*/ + /* height: 42px; */ - /*.two { + /* .two { width: 42px; - }*/ + } */ .three { - /*padding: 10px;*/ + /* padding: 10px; */ } }`; return withRandomFileEditor(contents, 'css', (editor, doc) => { @@ -641,14 +641,14 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { test('toggle comment with multiple cursors and whole node selected (CSS)', () => { const expectedContents = ` .one { - /*height: 42px;*/ + /* height: 42px; */ - /*.two { + /* .two { width: 42px; - }*/ + } */ .three { - /*padding: 10px;*/ + /* padding: 10px; */ } }`; return withRandomFileEditor(contents, 'css', (editor, doc) => { @@ -673,11 +673,11 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { test('toggle comment when multiple nodes are completely under single selection (CSS)', () => { const expectedContents = ` .one { - /*height: 42px; + /* height: 42px; .two { width: 42px; - }*/ + } */ .three { padding: 10px; @@ -701,11 +701,11 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { test('toggle comment when multiple nodes are partially under single selection (CSS)', () => { const expectedContents = ` .one { - /*height: 42px; + /* height: 42px; .two { width: 42px; - */ } + */ } .three { padding: 10px; @@ -726,4 +726,29 @@ suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => { }); }); -}); \ No newline at end of file + test('toggle comment doesn\'t fail when start and end nodes differ HTML', () => { + const contents = ` +
    +

    Hello

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

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

    `; +const htmlContentsForInlineWrapTests = ` + +`; + const wrapBlockElementExpected = ` `; +// technically a bug, but also a feature (requested behaviour) +// https://github.com/microsoft/vscode/issues/78015 const wrapInlineElementExpectedFormatFalse = ` `; @@ -73,51 +90,51 @@ suite('Tests for Wrap with Abbreviations', () => { const oldValueForSyntaxProfiles = workspace.getConfiguration('emmet').inspect('syntaxProfile'); test('Wrap with block element using multi cursor', () => { - return testWrapWithAbbreviation(multiCursors, 'div', wrapBlockElementExpected); + return testWrapWithAbbreviation(multiCursors, 'div', wrapBlockElementExpected, htmlContentsForBlockWrapTests); }); test('Wrap with inline element using multi cursor', () => { - return testWrapWithAbbreviation(multiCursors, 'span', wrapInlineElementExpected); + return testWrapWithAbbreviation(multiCursors, 'span', wrapInlineElementExpected, htmlContentsForInlineWrapTests); }); test('Wrap with snippet using multi cursor', () => { - return testWrapWithAbbreviation(multiCursors, 'a', wrapSnippetExpected); + return testWrapWithAbbreviation(multiCursors, 'a', wrapSnippetExpected, htmlContentsForBlockWrapTests); }); test('Wrap with multi line abbreviation using multi cursor', () => { - return testWrapWithAbbreviation(multiCursors, 'ul>li', wrapMultiLineAbbrExpected); + return testWrapWithAbbreviation(multiCursors, 'ul>li', wrapMultiLineAbbrExpected, htmlContentsForBlockWrapTests); }); test('Wrap with block element using multi cursor selection', () => { - return testWrapWithAbbreviation(multiCursorsWithSelection, 'div', wrapBlockElementExpected); + return testWrapWithAbbreviation(multiCursorsWithSelection, 'div', wrapBlockElementExpected, htmlContentsForBlockWrapTests); }); test('Wrap with inline element using multi cursor selection', () => { - return testWrapWithAbbreviation(multiCursorsWithSelection, 'span', wrapInlineElementExpected); + return testWrapWithAbbreviation(multiCursorsWithSelection, 'span', wrapInlineElementExpected, htmlContentsForInlineWrapTests); }); test('Wrap with snippet using multi cursor selection', () => { - return testWrapWithAbbreviation(multiCursorsWithSelection, 'a', wrapSnippetExpected); + return testWrapWithAbbreviation(multiCursorsWithSelection, 'a', wrapSnippetExpected, htmlContentsForBlockWrapTests); }); test('Wrap with multi line abbreviation using multi cursor selection', () => { - return testWrapWithAbbreviation(multiCursorsWithSelection, 'ul>li', wrapMultiLineAbbrExpected); + return testWrapWithAbbreviation(multiCursorsWithSelection, 'ul>li', wrapMultiLineAbbrExpected, htmlContentsForBlockWrapTests); }); test('Wrap with block element using multi cursor full line selection', () => { - return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'div', wrapBlockElementExpected); + return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'div', wrapBlockElementExpected, htmlContentsForBlockWrapTests); }); test('Wrap with inline element using multi cursor full line selection', () => { - return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'span', wrapInlineElementExpected); + return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'span', wrapInlineElementExpected, htmlContentsForInlineWrapTests); }); test('Wrap with snippet using multi cursor full line selection', () => { - return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'a', wrapSnippetExpected); + return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'a', wrapSnippetExpected, htmlContentsForBlockWrapTests); }); test('Wrap with multi line abbreviation using multi cursor full line selection', () => { - return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'ul>li', wrapMultiLineAbbrExpected); + return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'ul>li', wrapMultiLineAbbrExpected, htmlContentsForBlockWrapTests); }); test('Wrap with abbreviation and comment filter', () => { @@ -128,15 +145,31 @@ suite('Tests for Wrap with Abbreviations', () => { `; const expectedContents = ` `; return testWrapWithAbbreviation([new Selection(2, 0, 2, 0)], 'li.hello|c', expectedContents, contents); }); + test('Wrap with abbreviation link', () => { + const contents = ` + + `; + const expectedContents = ` + +
    + +
    +
    + `; + return testWrapWithAbbreviation([new Selection(1, 1, 1, 1)], 'a[href="https://example.com"]>div', expectedContents, contents); + }); + test('Wrap with abbreviation entire node when cursor is on opening tag', () => { const contents = ` ', 'adiv classname="">', fuzzyScore); + }); + test('Suggestion is not highlighted #85826', function () { assertMatches('SemanticTokens', 'SemanticTokensEdits', '^S^e^m^a^n^t^i^c^T^o^k^e^n^sEdits', fuzzyScore); assertMatches('SemanticTokens', 'SemanticTokensEdits', '^S^e^m^a^n^t^i^c^T^o^k^e^n^sEdits', fuzzyScoreGracefulAggressive); }); + + test('IntelliSense completion not correctly highlighting text in front of cursor #115250', function () { + assertMatches('lo', 'log', '^l^og', fuzzyScore); + assertMatches('.lo', 'log', '^l^og', anyScore); + assertMatches('.', 'log', 'log', anyScore); + }); }); diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index 5aac1fb98..59c52ce00 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -120,30 +120,30 @@ suite('Fuzzy Scorer', () => { // Assert scoring order let sortedScores = scores.concat().sort((a, b) => b[0] - a[0]); - assert.deepEqual(scores, sortedScores); + assert.deepStrictEqual(scores, sortedScores); // Assert scoring positions // let positions = scores[0][1]; - // assert.equal(positions.length, 'HelLo-World'.length); + // assert.strictEqual(positions.length, 'HelLo-World'.length); // positions = scores[2][1]; - // assert.equal(positions.length, 'HW'.length); - // assert.equal(positions[0], 0); - // assert.equal(positions[1], 6); + // assert.strictEqual(positions.length, 'HW'.length); + // assert.strictEqual(positions[0], 0); + // assert.strictEqual(positions[1], 6); }); test('score (non fuzzy)', function () { const target = 'HeLlo-World'; assert.ok(_doScore(target, 'HelLo-World', false)[0] > 0); - assert.equal(_doScore(target, 'HelLo-World', false)[1].length, 'HelLo-World'.length); + assert.strictEqual(_doScore(target, 'HelLo-World', false)[1].length, 'HelLo-World'.length); assert.ok(_doScore(target, 'hello-world', false)[0] > 0); - assert.equal(_doScore(target, 'HW', false)[0], 0); + assert.strictEqual(_doScore(target, 'HW', false)[0], 0); assert.ok(_doScore(target, 'h', false)[0] > 0); assert.ok(_doScore(target, 'ello', false)[0] > 0); assert.ok(_doScore(target, 'ld', false)[0] > 0); - assert.equal(_doScore(target, 'eo', false)[0], 0); + assert.strictEqual(_doScore(target, 'eo', false)[0], 0); }); test('scoreItem - matches are proper', function () { @@ -158,52 +158,52 @@ suite('Fuzzy Scorer', () => { // Path Identity const identityRes = scoreItem(resource, ResourceAccessor.getItemPath(resource), true, ResourceAccessor); assert.ok(identityRes.score); - assert.equal(identityRes.descriptionMatch!.length, 1); - assert.equal(identityRes.labelMatch!.length, 1); - assert.equal(identityRes.descriptionMatch![0].start, 0); - assert.equal(identityRes.descriptionMatch![0].end, ResourceAccessor.getItemDescription(resource).length); - assert.equal(identityRes.labelMatch![0].start, 0); - assert.equal(identityRes.labelMatch![0].end, ResourceAccessor.getItemLabel(resource).length); + assert.strictEqual(identityRes.descriptionMatch!.length, 1); + assert.strictEqual(identityRes.labelMatch!.length, 1); + assert.strictEqual(identityRes.descriptionMatch![0].start, 0); + assert.strictEqual(identityRes.descriptionMatch![0].end, ResourceAccessor.getItemDescription(resource).length); + assert.strictEqual(identityRes.labelMatch![0].start, 0); + assert.strictEqual(identityRes.labelMatch![0].end, ResourceAccessor.getItemLabel(resource).length); // Basename Prefix const basenamePrefixRes = scoreItem(resource, 'som', true, ResourceAccessor); assert.ok(basenamePrefixRes.score); assert.ok(!basenamePrefixRes.descriptionMatch); - assert.equal(basenamePrefixRes.labelMatch!.length, 1); - assert.equal(basenamePrefixRes.labelMatch![0].start, 0); - assert.equal(basenamePrefixRes.labelMatch![0].end, 'som'.length); + assert.strictEqual(basenamePrefixRes.labelMatch!.length, 1); + assert.strictEqual(basenamePrefixRes.labelMatch![0].start, 0); + assert.strictEqual(basenamePrefixRes.labelMatch![0].end, 'som'.length); // Basename Camelcase const basenameCamelcaseRes = scoreItem(resource, 'sF', true, ResourceAccessor); assert.ok(basenameCamelcaseRes.score); assert.ok(!basenameCamelcaseRes.descriptionMatch); - assert.equal(basenameCamelcaseRes.labelMatch!.length, 2); - assert.equal(basenameCamelcaseRes.labelMatch![0].start, 0); - assert.equal(basenameCamelcaseRes.labelMatch![0].end, 1); - assert.equal(basenameCamelcaseRes.labelMatch![1].start, 4); - assert.equal(basenameCamelcaseRes.labelMatch![1].end, 5); + assert.strictEqual(basenameCamelcaseRes.labelMatch!.length, 2); + assert.strictEqual(basenameCamelcaseRes.labelMatch![0].start, 0); + assert.strictEqual(basenameCamelcaseRes.labelMatch![0].end, 1); + assert.strictEqual(basenameCamelcaseRes.labelMatch![1].start, 4); + assert.strictEqual(basenameCamelcaseRes.labelMatch![1].end, 5); // Basename Match const basenameRes = scoreItem(resource, 'of', true, ResourceAccessor); assert.ok(basenameRes.score); assert.ok(!basenameRes.descriptionMatch); - assert.equal(basenameRes.labelMatch!.length, 2); - assert.equal(basenameRes.labelMatch![0].start, 1); - assert.equal(basenameRes.labelMatch![0].end, 2); - assert.equal(basenameRes.labelMatch![1].start, 4); - assert.equal(basenameRes.labelMatch![1].end, 5); + assert.strictEqual(basenameRes.labelMatch!.length, 2); + assert.strictEqual(basenameRes.labelMatch![0].start, 1); + assert.strictEqual(basenameRes.labelMatch![0].end, 2); + assert.strictEqual(basenameRes.labelMatch![1].start, 4); + assert.strictEqual(basenameRes.labelMatch![1].end, 5); // Path Match const pathRes = scoreItem(resource, 'xyz123', true, ResourceAccessor); assert.ok(pathRes.score); assert.ok(pathRes.descriptionMatch); assert.ok(pathRes.labelMatch); - assert.equal(pathRes.labelMatch!.length, 1); - assert.equal(pathRes.labelMatch![0].start, 8); - assert.equal(pathRes.labelMatch![0].end, 11); - assert.equal(pathRes.descriptionMatch!.length, 1); - assert.equal(pathRes.descriptionMatch![0].start, 1); - assert.equal(pathRes.descriptionMatch![0].end, 4); + assert.strictEqual(pathRes.labelMatch!.length, 1); + assert.strictEqual(pathRes.labelMatch![0].start, 8); + assert.strictEqual(pathRes.labelMatch![0].end, 11); + assert.strictEqual(pathRes.descriptionMatch!.length, 1); + assert.strictEqual(pathRes.descriptionMatch![0].start, 1); + assert.strictEqual(pathRes.descriptionMatch![0].end, 4); // No Match const noRes = scoreItem(resource, '987', true, ResourceAccessor); @@ -223,51 +223,51 @@ suite('Fuzzy Scorer', () => { let res1 = scoreItem(resource, 'xyz some', true, ResourceAccessor); assert.ok(res1.score); - assert.equal(res1.labelMatch?.length, 1); - assert.equal(res1.labelMatch![0].start, 0); - assert.equal(res1.labelMatch![0].end, 4); - assert.equal(res1.descriptionMatch?.length, 1); - assert.equal(res1.descriptionMatch![0].start, 1); - assert.equal(res1.descriptionMatch![0].end, 4); + assert.strictEqual(res1.labelMatch?.length, 1); + assert.strictEqual(res1.labelMatch![0].start, 0); + assert.strictEqual(res1.labelMatch![0].end, 4); + assert.strictEqual(res1.descriptionMatch?.length, 1); + assert.strictEqual(res1.descriptionMatch![0].start, 1); + assert.strictEqual(res1.descriptionMatch![0].end, 4); let res2 = scoreItem(resource, 'some xyz', true, ResourceAccessor); assert.ok(res2.score); - assert.equal(res1.score, res2.score); - assert.equal(res2.labelMatch?.length, 1); - assert.equal(res2.labelMatch![0].start, 0); - assert.equal(res2.labelMatch![0].end, 4); - assert.equal(res2.descriptionMatch?.length, 1); - assert.equal(res2.descriptionMatch![0].start, 1); - assert.equal(res2.descriptionMatch![0].end, 4); + assert.strictEqual(res1.score, res2.score); + assert.strictEqual(res2.labelMatch?.length, 1); + assert.strictEqual(res2.labelMatch![0].start, 0); + assert.strictEqual(res2.labelMatch![0].end, 4); + assert.strictEqual(res2.descriptionMatch?.length, 1); + assert.strictEqual(res2.descriptionMatch![0].start, 1); + assert.strictEqual(res2.descriptionMatch![0].end, 4); let res3 = scoreItem(resource, 'some xyz file file123', true, ResourceAccessor); assert.ok(res3.score); assert.ok(res3.score > res2.score); - assert.equal(res3.labelMatch?.length, 1); - assert.equal(res3.labelMatch![0].start, 0); - assert.equal(res3.labelMatch![0].end, 11); - assert.equal(res3.descriptionMatch?.length, 1); - assert.equal(res3.descriptionMatch![0].start, 1); - assert.equal(res3.descriptionMatch![0].end, 4); + assert.strictEqual(res3.labelMatch?.length, 1); + assert.strictEqual(res3.labelMatch![0].start, 0); + assert.strictEqual(res3.labelMatch![0].end, 11); + assert.strictEqual(res3.descriptionMatch?.length, 1); + assert.strictEqual(res3.descriptionMatch![0].start, 1); + assert.strictEqual(res3.descriptionMatch![0].end, 4); let res4 = scoreItem(resource, 'path z y', true, ResourceAccessor); assert.ok(res4.score); assert.ok(res4.score < res2.score); - assert.equal(res4.labelMatch?.length, 0); - assert.equal(res4.descriptionMatch?.length, 2); - assert.equal(res4.descriptionMatch![0].start, 2); - assert.equal(res4.descriptionMatch![0].end, 4); - assert.equal(res4.descriptionMatch![1].start, 10); - assert.equal(res4.descriptionMatch![1].end, 14); + assert.strictEqual(res4.labelMatch?.length, 0); + assert.strictEqual(res4.descriptionMatch?.length, 2); + assert.strictEqual(res4.descriptionMatch![0].start, 2); + assert.strictEqual(res4.descriptionMatch![0].end, 4); + assert.strictEqual(res4.descriptionMatch![1].start, 10); + assert.strictEqual(res4.descriptionMatch![1].end, 14); }); test('scoreItem - invalid input', function () { let res = scoreItem(null, null!, true, ResourceAccessor); - assert.equal(res.score, 0); + assert.strictEqual(res.score, 0); res = scoreItem(null, 'null', true, ResourceAccessor); - assert.equal(res.score, 0); + assert.strictEqual(res.score, 0); }); test('scoreItem - optimize for file paths', function () { @@ -280,12 +280,12 @@ suite('Fuzzy Scorer', () => { assert.ok(pathRes.score); assert.ok(pathRes.descriptionMatch); assert.ok(pathRes.labelMatch); - assert.equal(pathRes.labelMatch!.length, 1); - assert.equal(pathRes.labelMatch![0].start, 0); - assert.equal(pathRes.labelMatch![0].end, 7); - assert.equal(pathRes.descriptionMatch!.length, 1); - assert.equal(pathRes.descriptionMatch![0].start, 23); - assert.equal(pathRes.descriptionMatch![0].end, 26); + assert.strictEqual(pathRes.labelMatch!.length, 1); + assert.strictEqual(pathRes.labelMatch![0].start, 0); + assert.strictEqual(pathRes.labelMatch![0].end, 7); + assert.strictEqual(pathRes.descriptionMatch!.length, 1); + assert.strictEqual(pathRes.descriptionMatch![0].start, 23); + assert.strictEqual(pathRes.descriptionMatch![0].end, 26); }); test('scoreItem - avoid match scattering (bug #36119)', function () { @@ -295,9 +295,9 @@ suite('Fuzzy Scorer', () => { assert.ok(pathRes.score); assert.ok(pathRes.descriptionMatch); assert.ok(pathRes.labelMatch); - assert.equal(pathRes.labelMatch!.length, 1); - assert.equal(pathRes.labelMatch![0].start, 0); - assert.equal(pathRes.labelMatch![0].end, 9); + assert.strictEqual(pathRes.labelMatch!.length, 1); + assert.strictEqual(pathRes.labelMatch![0].start, 0); + assert.strictEqual(pathRes.labelMatch![0].end, 9); }); test('scoreItem - prefers more compact matches', function () { @@ -309,11 +309,11 @@ suite('Fuzzy Scorer', () => { assert.ok(res.score); assert.ok(res.descriptionMatch); assert.ok(!res.labelMatch!.length); - assert.equal(res.descriptionMatch!.length, 2); - assert.equal(res.descriptionMatch![0].start, 11); - assert.equal(res.descriptionMatch![0].end, 12); - assert.equal(res.descriptionMatch![1].start, 13); - assert.equal(res.descriptionMatch![1].end, 14); + assert.strictEqual(res.descriptionMatch!.length, 2); + assert.strictEqual(res.descriptionMatch![0].start, 11); + assert.strictEqual(res.descriptionMatch![0].end, 12); + assert.strictEqual(res.descriptionMatch![1].start, 13); + assert.strictEqual(res.descriptionMatch![1].end, 14); }); test('scoreItem - proper target offset', function () { @@ -328,9 +328,9 @@ suite('Fuzzy Scorer', () => { const res = scoreItem(resource, 'de', true, ResourceAccessor); - assert.equal(res.labelMatch!.length, 1); - assert.equal(res.labelMatch![0].start, 1); - assert.equal(res.labelMatch![0].end, 3); + assert.strictEqual(res.labelMatch!.length, 1); + assert.strictEqual(res.labelMatch![0].start, 1); + assert.strictEqual(res.labelMatch![0].end, 3); }); test('scoreItem - proper target offset #3', function () { @@ -338,19 +338,19 @@ suite('Fuzzy Scorer', () => { const res = scoreItem(resource, 'debug', true, ResourceAccessor); - assert.equal(res.descriptionMatch!.length, 3); - assert.equal(res.descriptionMatch![0].start, 9); - assert.equal(res.descriptionMatch![0].end, 10); - assert.equal(res.descriptionMatch![1].start, 36); - assert.equal(res.descriptionMatch![1].end, 37); - assert.equal(res.descriptionMatch![2].start, 40); - assert.equal(res.descriptionMatch![2].end, 41); + assert.strictEqual(res.descriptionMatch!.length, 3); + assert.strictEqual(res.descriptionMatch![0].start, 9); + assert.strictEqual(res.descriptionMatch![0].end, 10); + assert.strictEqual(res.descriptionMatch![1].start, 36); + assert.strictEqual(res.descriptionMatch![1].end, 37); + assert.strictEqual(res.descriptionMatch![2].start, 40); + assert.strictEqual(res.descriptionMatch![2].end, 41); - assert.equal(res.labelMatch!.length, 2); - assert.equal(res.labelMatch![0].start, 9); - assert.equal(res.labelMatch![0].end, 10); - assert.equal(res.labelMatch![1].start, 20); - assert.equal(res.labelMatch![1].end, 21); + assert.strictEqual(res.labelMatch!.length, 2); + assert.strictEqual(res.labelMatch![0].start, 9); + assert.strictEqual(res.labelMatch![0].end, 10); + assert.strictEqual(res.labelMatch![1].start, 20); + assert.strictEqual(res.labelMatch![1].end, 21); }); test('scoreItem - no match unless query contained in sequence', function () { @@ -394,27 +394,27 @@ suite('Fuzzy Scorer', () => { let query = ResourceAccessor.getItemPath(resourceA); let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // Full resource B path query = ResourceAccessor.getItemPath(resourceB); res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - basename prefix', function () { @@ -426,27 +426,27 @@ suite('Fuzzy Scorer', () => { let query = ResourceAccessor.getItemLabel(resourceA); let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // Full resource B basename query = ResourceAccessor.getItemLabel(resourceB); res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - basename camelcase', function () { @@ -458,27 +458,27 @@ suite('Fuzzy Scorer', () => { let query = 'fA'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // resource B camelcase query = 'fB'; res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - basename scores', function () { @@ -490,27 +490,27 @@ suite('Fuzzy Scorer', () => { let query = 'fileA'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // Resource B part of basename query = 'fileB'; res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - path scores', function () { @@ -522,27 +522,27 @@ suite('Fuzzy Scorer', () => { let query = 'pathfileA'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // Resource B part of path query = 'pathfileB'; res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - prefer shorter basenames', function () { @@ -554,14 +554,14 @@ suite('Fuzzy Scorer', () => { let query = 'somepath'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - prefer shorter basenames (match on basename)', function () { @@ -573,14 +573,14 @@ suite('Fuzzy Scorer', () => { let query = 'file'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceC); - assert.equal(res[2], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceC); + assert.strictEqual(res[2], resourceB); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceC); - assert.equal(res[2], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceC); + assert.strictEqual(res[2], resourceB); }); test('compareFilesByScore - prefer shorter paths', function () { @@ -592,14 +592,14 @@ suite('Fuzzy Scorer', () => { let query = 'somepath'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - prefer shorter paths (bug #17443)', function () { @@ -610,9 +610,9 @@ suite('Fuzzy Scorer', () => { let query = 'co/te'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - prefer matches in label over description if scores are otherwise equal', function () { @@ -622,8 +622,8 @@ suite('Fuzzy Scorer', () => { let query = 'partsquick'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - prefer camel case matches', function () { @@ -632,12 +632,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['npe', 'NPE']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); } }); @@ -648,12 +648,12 @@ suite('Fuzzy Scorer', () => { let query = 'AH'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - prefer more compact matches (label)', function () { @@ -663,12 +663,12 @@ suite('Fuzzy Scorer', () => { let query = 'xp'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - prefer more compact matches (path)', function () { @@ -678,12 +678,12 @@ suite('Fuzzy Scorer', () => { let query = 'xp'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - prefer more compact matches (label and path)', function () { @@ -693,12 +693,12 @@ suite('Fuzzy Scorer', () => { let query = 'exfile'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - avoid match scattering (bug #34210)', function () { @@ -710,18 +710,18 @@ suite('Fuzzy Scorer', () => { let query = isWindows ? 'modu1\\index.js' : 'modu1/index.js'; let res = [resourceA, resourceB, resourceC, resourceD].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); res = [resourceC, resourceB, resourceA, resourceD].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); query = isWindows ? 'un1\\index.js' : 'un1/index.js'; res = [resourceA, resourceB, resourceC, resourceD].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceC, resourceB, resourceA, resourceD].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #21019 1.)', function () { @@ -732,10 +732,10 @@ suite('Fuzzy Scorer', () => { let query = 'StatVideoindex'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); }); test('compareFilesByScore - avoid match scattering (bug #21019 2.)', function () { @@ -745,10 +745,10 @@ suite('Fuzzy Scorer', () => { let query = 'reproreduxts'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #26649)', function () { @@ -759,10 +759,10 @@ suite('Fuzzy Scorer', () => { let query = 'bookpageIndex'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); }); test('compareFilesByScore - avoid match scattering (bug #33247)', function () { @@ -772,10 +772,10 @@ suite('Fuzzy Scorer', () => { let query = isWindows ? 'ui\\icons' : 'ui/icons'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #33247 comment)', function () { @@ -785,10 +785,10 @@ suite('Fuzzy Scorer', () => { let query = isWindows ? 'ui\\input\\index' : 'ui/input/index'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #36166)', function () { @@ -798,10 +798,10 @@ suite('Fuzzy Scorer', () => { let query = 'djancosig'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #32918)', function () { @@ -812,14 +812,14 @@ suite('Fuzzy Scorer', () => { let query = 'protectedconfig.php'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceC); - assert.equal(res[2], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceC); + assert.strictEqual(res[2], resourceB); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceC); - assert.equal(res[2], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceC); + assert.strictEqual(res[2], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #14879)', function () { @@ -829,10 +829,10 @@ suite('Fuzzy Scorer', () => { let query = 'gradientmain'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #14727 1)', function () { @@ -842,10 +842,10 @@ suite('Fuzzy Scorer', () => { let query = 'abc'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #14727 2)', function () { @@ -855,10 +855,10 @@ suite('Fuzzy Scorer', () => { let query = 'xyz'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #18381)', function () { @@ -868,10 +868,10 @@ suite('Fuzzy Scorer', () => { let query = 'async'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #35572)', function () { @@ -881,10 +881,10 @@ suite('Fuzzy Scorer', () => { let query = 'partisettings'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #36810)', function () { @@ -894,10 +894,10 @@ suite('Fuzzy Scorer', () => { let query = 'tipsindex.cshtml'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - prefer shorter hit (bug #20546)', function () { @@ -907,10 +907,10 @@ suite('Fuzzy Scorer', () => { let query = 'listview'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #12095)', function () { @@ -921,10 +921,10 @@ suite('Fuzzy Scorer', () => { let query = 'filesexplorerview.ts'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceA, resourceC, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - prefer case match (bug #96122)', function () { @@ -934,10 +934,10 @@ suite('Fuzzy Scorer', () => { let query = 'Lists.php'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - prefer shorter match (bug #103052) - foo bar', function () { @@ -946,12 +946,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['foo bar', 'foobar']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); } }); @@ -961,12 +961,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['payment model', 'paymentmodel']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); } }); @@ -976,12 +976,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['color js', 'colorjs']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); } }); @@ -992,22 +992,22 @@ suite('Fuzzy Scorer', () => { let query = 'Color'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); query = 'color'; res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); }); test('compareFilesByScore - prefer prefix (bug #103052)', function () { @@ -1017,12 +1017,12 @@ suite('Fuzzy Scorer', () => { let query = 'smoke main.ts'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); }); test('compareFilesByScore - boost better prefix match if multiple queries are used', function () { @@ -1031,12 +1031,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['workbench.ts browser', 'browser workbench.ts', 'browser workbench', 'workbench browser']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); } }); @@ -1046,12 +1046,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['window browser', 'window.ts browser']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); } }); @@ -1061,73 +1061,73 @@ suite('Fuzzy Scorer', () => { for (const query of ['m life, life m']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); } }); test('prepareQuery', () => { - assert.equal(scorer.prepareQuery(' f*a ').normalized, 'fa'); - assert.equal(scorer.prepareQuery('model Tester.ts').original, 'model Tester.ts'); - assert.equal(scorer.prepareQuery('model Tester.ts').originalLowercase, 'model Tester.ts'.toLowerCase()); - assert.equal(scorer.prepareQuery('model Tester.ts').normalized, 'modelTester.ts'); - assert.equal(scorer.prepareQuery('Model Tester.ts').normalizedLowercase, 'modeltester.ts'); - assert.equal(scorer.prepareQuery('ModelTester.ts').containsPathSeparator, false); - assert.equal(scorer.prepareQuery('Model' + sep + 'Tester.ts').containsPathSeparator, true); + assert.strictEqual(scorer.prepareQuery(' f*a ').normalized, 'fa'); + assert.strictEqual(scorer.prepareQuery('model Tester.ts').original, 'model Tester.ts'); + assert.strictEqual(scorer.prepareQuery('model Tester.ts').originalLowercase, 'model Tester.ts'.toLowerCase()); + assert.strictEqual(scorer.prepareQuery('model Tester.ts').normalized, 'modelTester.ts'); + assert.strictEqual(scorer.prepareQuery('Model Tester.ts').normalizedLowercase, 'modeltester.ts'); + assert.strictEqual(scorer.prepareQuery('ModelTester.ts').containsPathSeparator, false); + assert.strictEqual(scorer.prepareQuery('Model' + sep + 'Tester.ts').containsPathSeparator, true); // with spaces let query = scorer.prepareQuery('He*llo World'); - assert.equal(query.original, 'He*llo World'); - assert.equal(query.normalized, 'HelloWorld'); - assert.equal(query.normalizedLowercase, 'HelloWorld'.toLowerCase()); - assert.equal(query.values?.length, 2); - assert.equal(query.values?.[0].original, 'He*llo'); - assert.equal(query.values?.[0].normalized, 'Hello'); - assert.equal(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase()); - assert.equal(query.values?.[1].original, 'World'); - assert.equal(query.values?.[1].normalized, 'World'); - assert.equal(query.values?.[1].normalizedLowercase, 'World'.toLowerCase()); + assert.strictEqual(query.original, 'He*llo World'); + assert.strictEqual(query.normalized, 'HelloWorld'); + assert.strictEqual(query.normalizedLowercase, 'HelloWorld'.toLowerCase()); + assert.strictEqual(query.values?.length, 2); + assert.strictEqual(query.values?.[0].original, 'He*llo'); + assert.strictEqual(query.values?.[0].normalized, 'Hello'); + assert.strictEqual(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase()); + assert.strictEqual(query.values?.[1].original, 'World'); + assert.strictEqual(query.values?.[1].normalized, 'World'); + assert.strictEqual(query.values?.[1].normalizedLowercase, 'World'.toLowerCase()); let restoredQuery = scorer.pieceToQuery(query.values!); - assert.equal(restoredQuery.original, query.original); - assert.equal(restoredQuery.values?.length, query.values?.length); - assert.equal(restoredQuery.containsPathSeparator, query.containsPathSeparator); + assert.strictEqual(restoredQuery.original, query.original); + assert.strictEqual(restoredQuery.values?.length, query.values?.length); + assert.strictEqual(restoredQuery.containsPathSeparator, query.containsPathSeparator); // with spaces that are empty query = scorer.prepareQuery(' Hello World '); - assert.equal(query.original, ' Hello World '); - assert.equal(query.originalLowercase, ' Hello World '.toLowerCase()); - assert.equal(query.normalized, 'HelloWorld'); - assert.equal(query.normalizedLowercase, 'HelloWorld'.toLowerCase()); - assert.equal(query.values?.length, 2); - assert.equal(query.values?.[0].original, 'Hello'); - assert.equal(query.values?.[0].originalLowercase, 'Hello'.toLowerCase()); - assert.equal(query.values?.[0].normalized, 'Hello'); - assert.equal(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase()); - assert.equal(query.values?.[1].original, 'World'); - assert.equal(query.values?.[1].originalLowercase, 'World'.toLowerCase()); - assert.equal(query.values?.[1].normalized, 'World'); - assert.equal(query.values?.[1].normalizedLowercase, 'World'.toLowerCase()); + assert.strictEqual(query.original, ' Hello World '); + assert.strictEqual(query.originalLowercase, ' Hello World '.toLowerCase()); + assert.strictEqual(query.normalized, 'HelloWorld'); + assert.strictEqual(query.normalizedLowercase, 'HelloWorld'.toLowerCase()); + assert.strictEqual(query.values?.length, 2); + assert.strictEqual(query.values?.[0].original, 'Hello'); + assert.strictEqual(query.values?.[0].originalLowercase, 'Hello'.toLowerCase()); + assert.strictEqual(query.values?.[0].normalized, 'Hello'); + assert.strictEqual(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase()); + assert.strictEqual(query.values?.[1].original, 'World'); + assert.strictEqual(query.values?.[1].originalLowercase, 'World'.toLowerCase()); + assert.strictEqual(query.values?.[1].normalized, 'World'); + assert.strictEqual(query.values?.[1].normalizedLowercase, 'World'.toLowerCase()); // Path related if (isWindows) { - assert.equal(scorer.prepareQuery('C:\\some\\path').pathNormalized, 'C:\\some\\path'); - assert.equal(scorer.prepareQuery('C:\\some\\path').normalized, 'C:\\some\\path'); - assert.equal(scorer.prepareQuery('C:\\some\\path').containsPathSeparator, true); - assert.equal(scorer.prepareQuery('C:/some/path').pathNormalized, 'C:\\some\\path'); - assert.equal(scorer.prepareQuery('C:/some/path').normalized, 'C:\\some\\path'); - assert.equal(scorer.prepareQuery('C:/some/path').containsPathSeparator, true); + assert.strictEqual(scorer.prepareQuery('C:\\some\\path').pathNormalized, 'C:\\some\\path'); + assert.strictEqual(scorer.prepareQuery('C:\\some\\path').normalized, 'C:\\some\\path'); + assert.strictEqual(scorer.prepareQuery('C:\\some\\path').containsPathSeparator, true); + assert.strictEqual(scorer.prepareQuery('C:/some/path').pathNormalized, 'C:\\some\\path'); + assert.strictEqual(scorer.prepareQuery('C:/some/path').normalized, 'C:\\some\\path'); + assert.strictEqual(scorer.prepareQuery('C:/some/path').containsPathSeparator, true); } else { - assert.equal(scorer.prepareQuery('/some/path').pathNormalized, '/some/path'); - assert.equal(scorer.prepareQuery('/some/path').normalized, '/some/path'); - assert.equal(scorer.prepareQuery('/some/path').containsPathSeparator, true); - assert.equal(scorer.prepareQuery('\\some\\path').pathNormalized, '/some/path'); - assert.equal(scorer.prepareQuery('\\some\\path').normalized, '/some/path'); - assert.equal(scorer.prepareQuery('\\some\\path').containsPathSeparator, true); + assert.strictEqual(scorer.prepareQuery('/some/path').pathNormalized, '/some/path'); + assert.strictEqual(scorer.prepareQuery('/some/path').normalized, '/some/path'); + assert.strictEqual(scorer.prepareQuery('/some/path').containsPathSeparator, true); + assert.strictEqual(scorer.prepareQuery('\\some\\path').pathNormalized, '/some/path'); + assert.strictEqual(scorer.prepareQuery('\\some\\path').normalized, '/some/path'); + assert.strictEqual(scorer.prepareQuery('\\some\\path').containsPathSeparator, true); } }); @@ -1138,18 +1138,18 @@ suite('Fuzzy Scorer', () => { let [score, matches] = _doScore2(offset === 0 ? target : `123${target}`, 'HeLlo-World', offset); assert.ok(score); - assert.equal(matches.length, 1); - assert.equal(matches[0].start, 0 + offset); - assert.equal(matches[0].end, target.length + offset); + assert.strictEqual(matches.length, 1); + assert.strictEqual(matches[0].start, 0 + offset); + assert.strictEqual(matches[0].end, target.length + offset); [score, matches] = _doScore2(offset === 0 ? target : `123${target}`, 'HW', offset); assert.ok(score); - assert.equal(matches.length, 2); - assert.equal(matches[0].start, 0 + offset); - assert.equal(matches[0].end, 1 + offset); - assert.equal(matches[1].start, 6 + offset); - assert.equal(matches[1].end, 7 + offset); + assert.strictEqual(matches.length, 2); + assert.strictEqual(matches[0].start, 0 + offset); + assert.strictEqual(matches[0].end, 1 + offset); + assert.strictEqual(matches[1].start, 6 + offset); + assert.strictEqual(matches[1].end, 7 + offset); } }); @@ -1169,8 +1169,8 @@ suite('Fuzzy Scorer', () => { const firstAndSecondSingleMatch = firstAndSecondSingleMatches[i]; if (multiMatch && firstAndSecondSingleMatch) { - assert.equal(multiMatch.start, firstAndSecondSingleMatch.start); - assert.equal(multiMatch.end, firstAndSecondSingleMatch.end); + assert.strictEqual(multiMatch.start, firstAndSecondSingleMatch.start); + assert.strictEqual(multiMatch.end, firstAndSecondSingleMatch.end); } else { assert.fail(); } @@ -1178,8 +1178,8 @@ suite('Fuzzy Scorer', () => { } function assertNoScore() { - assert.equal(multiScore, undefined); - assert.equal(multiMatches.length, 0); + assert.strictEqual(multiScore, undefined); + assert.strictEqual(multiMatches.length, 0); } assertScore(); diff --git a/src/vs/base/test/node/glob.test.ts b/src/vs/base/test/common/glob.test.ts similarity index 98% rename from src/vs/base/test/node/glob.test.ts rename to src/vs/base/test/common/glob.test.ts index 2c0288e38..25b838853 100644 --- a/src/vs/base/test/node/glob.test.ts +++ b/src/vs/base/test/common/glob.test.ts @@ -63,10 +63,12 @@ suite('Glob', () => { function assertGlobMatch(pattern: string | glob.IRelativePattern, input: string) { assert(glob.match(pattern, input), `${pattern} should match ${input}`); + assert(glob.match(pattern, nativeSep(input)), `${pattern} should match ${nativeSep(input)}`); } function assertNoGlobMatch(pattern: string | glob.IRelativePattern, input: string) { assert(!glob.match(pattern, input), `${pattern} should not match ${input}`); + assert(!glob.match(pattern, nativeSep(input)), `${pattern} should not match ${nativeSep(input)}`); } test('simple', () => { @@ -538,9 +540,13 @@ suite('Glob', () => { }); test('full path', function () { - let p = 'testing/this/foo.txt'; + assertGlobMatch('testing/this/foo.txt', 'testing/this/foo.txt'); + // assertGlobMatch('testing/this/foo.txt', 'testing\\this\\foo.txt'); + }); - assert(glob.match(p, nativeSep('testing/this/foo.txt'))); + test('ending path', function () { + assertGlobMatch('**/testing/this/foo.txt', 'some/path/testing/this/foo.txt'); + // assertGlobMatch('**/testing/this/foo.txt', 'some\\path\\testing\\this\\foo.txt'); }); test('prefix agnostic', function () { diff --git a/src/vs/base/test/common/iconLabels.test.ts b/src/vs/base/test/common/iconLabels.test.ts new file mode 100644 index 000000000..998c68971 --- /dev/null +++ b/src/vs/base/test/common/iconLabels.test.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { IMatch } from 'vs/base/common/filters'; +import { matchesFuzzyIconAware, parseLabelWithIcons, IParsedLabelWithIcons, stripIcons } from 'vs/base/common/iconLabels'; + +export interface IIconFilter { + // Returns null if word doesn't match. + (query: string, target: IParsedLabelWithIcons): IMatch[] | null; +} + +function filterOk(filter: IIconFilter, word: string, target: IParsedLabelWithIcons, highlights?: { start: number; end: number; }[]) { + let r = filter(word, target); + assert(r); + if (highlights) { + assert.deepEqual(r, highlights); + } +} + +suite('Icon Labels', () => { + test('matchesFuzzyIconAware', () => { + + // Camel Case + + filterOk(matchesFuzzyIconAware, 'ccr', parseLabelWithIcons('$(codicon)CamelCaseRocks$(codicon)'), [ + { start: 10, end: 11 }, + { start: 15, end: 16 }, + { start: 19, end: 20 } + ]); + + filterOk(matchesFuzzyIconAware, 'ccr', parseLabelWithIcons('$(codicon) CamelCaseRocks $(codicon)'), [ + { start: 11, end: 12 }, + { start: 16, end: 17 }, + { start: 20, end: 21 } + ]); + + filterOk(matchesFuzzyIconAware, 'iut', parseLabelWithIcons('$(codicon) Indent $(octico) Using $(octic) Tpaces'), [ + { start: 11, end: 12 }, + { start: 28, end: 29 }, + { start: 43, end: 44 }, + ]); + + // Prefix + + filterOk(matchesFuzzyIconAware, 'using', parseLabelWithIcons('$(codicon) Indent Using Spaces'), [ + { start: 18, end: 23 }, + ]); + + // Broken Codicon + + filterOk(matchesFuzzyIconAware, 'codicon', parseLabelWithIcons('This $(codicon Indent Using Spaces'), [ + { start: 7, end: 14 }, + ]); + + filterOk(matchesFuzzyIconAware, 'indent', parseLabelWithIcons('This $codicon Indent Using Spaces'), [ + { start: 14, end: 20 }, + ]); + + // Testing #59343 + filterOk(matchesFuzzyIconAware, 'unt', parseLabelWithIcons('$(primitive-dot) $(file-text) Untitled-1'), [ + { start: 30, end: 33 }, + ]); + }); + + test('stripIcons', () => { + assert.equal(stripIcons('Hello World'), 'Hello World'); + assert.equal(stripIcons('$(Hello World'), '$(Hello World'); + assert.equal(stripIcons('$(Hello) World'), ' World'); + assert.equal(stripIcons('$(Hello) W$(oi)rld'), ' Wrld'); + }); +}); diff --git a/src/vs/base/test/common/keyCodes.test.ts b/src/vs/base/test/common/keyCodes.test.ts index f8b2b55a4..f5c5bcdcc 100644 --- a/src/vs/base/test/common/keyCodes.test.ts +++ b/src/vs/base/test/common/keyCodes.test.ts @@ -10,7 +10,7 @@ import { OperatingSystem } from 'vs/base/common/platform'; suite('keyCodes', () => { function testBinaryEncoding(expected: Keybinding | null, k: number, OS: OperatingSystem): void { - assert.deepEqual(createKeybinding(k, OS), expected); + assert.deepStrictEqual(createKeybinding(k, OS), expected); } test('MAC binary encoding', () => { diff --git a/src/vs/base/test/common/labels.test.ts b/src/vs/base/test/common/labels.test.ts index a3e54c162..d7f0a401f 100644 --- a/src/vs/base/test/common/labels.test.ts +++ b/src/vs/base/test/common/labels.test.ts @@ -8,106 +8,98 @@ import * as labels from 'vs/base/common/labels'; import * as platform from 'vs/base/common/platform'; suite('Labels', () => { - test('shorten - windows', () => { - if (!platform.isWindows) { - assert.ok(true); - return; - } + (!platform.isWindows ? test.skip : test)('shorten - windows', () => { // nothing to shorten - assert.deepEqual(labels.shorten(['a']), ['a']); - assert.deepEqual(labels.shorten(['a', 'b']), ['a', 'b']); - assert.deepEqual(labels.shorten(['a', 'b', 'c']), ['a', 'b', 'c']); + assert.deepStrictEqual(labels.shorten(['a']), ['a']); + assert.deepStrictEqual(labels.shorten(['a', 'b']), ['a', 'b']); + assert.deepStrictEqual(labels.shorten(['a', 'b', 'c']), ['a', 'b', 'c']); // completely different paths - assert.deepEqual(labels.shorten(['a\\b', 'c\\d', 'e\\f']), ['…\\b', '…\\d', '…\\f']); + assert.deepStrictEqual(labels.shorten(['a\\b', 'c\\d', 'e\\f']), ['…\\b', '…\\d', '…\\f']); // same beginning - assert.deepEqual(labels.shorten(['a', 'a\\b']), ['a', '…\\b']); - assert.deepEqual(labels.shorten(['a\\b', 'a\\b\\c']), ['…\\b', '…\\c']); - assert.deepEqual(labels.shorten(['a', 'a\\b', 'a\\b\\c']), ['a', '…\\b', '…\\c']); - assert.deepEqual(labels.shorten(['x:\\a\\b', 'x:\\a\\c']), ['x:\\…\\b', 'x:\\…\\c']); - assert.deepEqual(labels.shorten(['\\\\a\\b', '\\\\a\\c']), ['\\\\a\\b', '\\\\a\\c']); + assert.deepStrictEqual(labels.shorten(['a', 'a\\b']), ['a', '…\\b']); + assert.deepStrictEqual(labels.shorten(['a\\b', 'a\\b\\c']), ['…\\b', '…\\c']); + assert.deepStrictEqual(labels.shorten(['a', 'a\\b', 'a\\b\\c']), ['a', '…\\b', '…\\c']); + assert.deepStrictEqual(labels.shorten(['x:\\a\\b', 'x:\\a\\c']), ['x:\\…\\b', 'x:\\…\\c']); + assert.deepStrictEqual(labels.shorten(['\\\\a\\b', '\\\\a\\c']), ['\\\\a\\b', '\\\\a\\c']); // same ending - assert.deepEqual(labels.shorten(['a', 'b\\a']), ['a', 'b\\…']); - assert.deepEqual(labels.shorten(['a\\b\\c', 'd\\b\\c']), ['a\\…', 'd\\…']); - assert.deepEqual(labels.shorten(['a\\b\\c\\d', 'f\\b\\c\\d']), ['a\\…', 'f\\…']); - assert.deepEqual(labels.shorten(['d\\e\\a\\b\\c', 'd\\b\\c']), ['…\\a\\…', 'd\\b\\…']); - assert.deepEqual(labels.shorten(['a\\b\\c\\d', 'a\\f\\b\\c\\d']), ['a\\b\\…', '…\\f\\…']); - assert.deepEqual(labels.shorten(['a\\b\\a', 'b\\b\\a']), ['a\\b\\…', 'b\\b\\…']); - assert.deepEqual(labels.shorten(['d\\f\\a\\b\\c', 'h\\d\\b\\c']), ['…\\a\\…', 'h\\…']); - assert.deepEqual(labels.shorten(['a\\b\\c', 'x:\\0\\a\\b\\c']), ['a\\b\\c', 'x:\\0\\…']); - assert.deepEqual(labels.shorten(['x:\\a\\b\\c', 'x:\\0\\a\\b\\c']), ['x:\\a\\…', 'x:\\0\\…']); - assert.deepEqual(labels.shorten(['x:\\a\\b', 'y:\\a\\b']), ['x:\\…', 'y:\\…']); - assert.deepEqual(labels.shorten(['x:\\a', 'x:\\c']), ['x:\\a', 'x:\\c']); - assert.deepEqual(labels.shorten(['x:\\a\\b', 'y:\\x\\a\\b']), ['x:\\…', 'y:\\…']); - assert.deepEqual(labels.shorten(['\\\\x\\b', '\\\\y\\b']), ['\\\\x\\…', '\\\\y\\…']); - assert.deepEqual(labels.shorten(['\\\\x\\a', '\\\\x\\b']), ['\\\\x\\a', '\\\\x\\b']); + assert.deepStrictEqual(labels.shorten(['a', 'b\\a']), ['a', 'b\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c', 'd\\b\\c']), ['a\\…', 'd\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c\\d', 'f\\b\\c\\d']), ['a\\…', 'f\\…']); + assert.deepStrictEqual(labels.shorten(['d\\e\\a\\b\\c', 'd\\b\\c']), ['…\\a\\…', 'd\\b\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c\\d', 'a\\f\\b\\c\\d']), ['a\\b\\…', '…\\f\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\a', 'b\\b\\a']), ['a\\b\\…', 'b\\b\\…']); + assert.deepStrictEqual(labels.shorten(['d\\f\\a\\b\\c', 'h\\d\\b\\c']), ['…\\a\\…', 'h\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c', 'x:\\0\\a\\b\\c']), ['a\\b\\c', 'x:\\0\\…']); + assert.deepStrictEqual(labels.shorten(['x:\\a\\b\\c', 'x:\\0\\a\\b\\c']), ['x:\\a\\…', 'x:\\0\\…']); + assert.deepStrictEqual(labels.shorten(['x:\\a\\b', 'y:\\a\\b']), ['x:\\…', 'y:\\…']); + assert.deepStrictEqual(labels.shorten(['x:\\a', 'x:\\c']), ['x:\\a', 'x:\\c']); + assert.deepStrictEqual(labels.shorten(['x:\\a\\b', 'y:\\x\\a\\b']), ['x:\\…', 'y:\\…']); + assert.deepStrictEqual(labels.shorten(['\\\\x\\b', '\\\\y\\b']), ['\\\\x\\…', '\\\\y\\…']); + assert.deepStrictEqual(labels.shorten(['\\\\x\\a', '\\\\x\\b']), ['\\\\x\\a', '\\\\x\\b']); // same name ending - assert.deepEqual(labels.shorten(['a\\b', 'a\\c', 'a\\e-b']), ['…\\b', '…\\c', '…\\e-b']); + assert.deepStrictEqual(labels.shorten(['a\\b', 'a\\c', 'a\\e-b']), ['…\\b', '…\\c', '…\\e-b']); // same in the middle - assert.deepEqual(labels.shorten(['a\\b\\c', 'd\\b\\e']), ['…\\c', '…\\e']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c', 'd\\b\\e']), ['…\\c', '…\\e']); // case-sensetive - assert.deepEqual(labels.shorten(['a\\b\\c', 'd\\b\\C']), ['…\\c', '…\\C']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c', 'd\\b\\C']), ['…\\c', '…\\C']); // empty or null - assert.deepEqual(labels.shorten(['', null!]), ['.\\', null]); + assert.deepStrictEqual(labels.shorten(['', null!]), ['.\\', null]); - assert.deepEqual(labels.shorten(['a', 'a\\b', 'a\\b\\c', 'd\\b\\c', 'd\\b']), ['a', 'a\\b', 'a\\b\\c', 'd\\b\\c', 'd\\b']); - assert.deepEqual(labels.shorten(['a', 'a\\b', 'b']), ['a', 'a\\b', 'b']); - assert.deepEqual(labels.shorten(['', 'a', 'b', 'b\\c', 'a\\c']), ['.\\', 'a', 'b', 'b\\c', 'a\\c']); - assert.deepEqual(labels.shorten(['src\\vs\\workbench\\parts\\execution\\electron-browser', 'src\\vs\\workbench\\parts\\execution\\electron-browser\\something', 'src\\vs\\workbench\\parts\\terminal\\electron-browser']), ['…\\execution\\electron-browser', '…\\something', '…\\terminal\\…']); + assert.deepStrictEqual(labels.shorten(['a', 'a\\b', 'a\\b\\c', 'd\\b\\c', 'd\\b']), ['a', 'a\\b', 'a\\b\\c', 'd\\b\\c', 'd\\b']); + assert.deepStrictEqual(labels.shorten(['a', 'a\\b', 'b']), ['a', 'a\\b', 'b']); + assert.deepStrictEqual(labels.shorten(['', 'a', 'b', 'b\\c', 'a\\c']), ['.\\', 'a', 'b', 'b\\c', 'a\\c']); + assert.deepStrictEqual(labels.shorten(['src\\vs\\workbench\\parts\\execution\\electron-browser', 'src\\vs\\workbench\\parts\\execution\\electron-browser\\something', 'src\\vs\\workbench\\parts\\terminal\\electron-browser']), ['…\\execution\\electron-browser', '…\\something', '…\\terminal\\…']); }); - test('shorten - not windows', () => { - if (platform.isWindows) { - assert.ok(true); - return; - } + (platform.isWindows ? test.skip : test)('shorten - not windows', () => { // nothing to shorten - assert.deepEqual(labels.shorten(['a']), ['a']); - assert.deepEqual(labels.shorten(['a', 'b']), ['a', 'b']); - assert.deepEqual(labels.shorten(['a', 'b', 'c']), ['a', 'b', 'c']); + assert.deepStrictEqual(labels.shorten(['a']), ['a']); + assert.deepStrictEqual(labels.shorten(['a', 'b']), ['a', 'b']); + assert.deepStrictEqual(labels.shorten(['a', 'b', 'c']), ['a', 'b', 'c']); // completely different paths - assert.deepEqual(labels.shorten(['a/b', 'c/d', 'e/f']), ['…/b', '…/d', '…/f']); + assert.deepStrictEqual(labels.shorten(['a/b', 'c/d', 'e/f']), ['…/b', '…/d', '…/f']); // same beginning - assert.deepEqual(labels.shorten(['a', 'a/b']), ['a', '…/b']); - assert.deepEqual(labels.shorten(['a/b', 'a/b/c']), ['…/b', '…/c']); - assert.deepEqual(labels.shorten(['a', 'a/b', 'a/b/c']), ['a', '…/b', '…/c']); - assert.deepEqual(labels.shorten(['/a/b', '/a/c']), ['/a/b', '/a/c']); + assert.deepStrictEqual(labels.shorten(['a', 'a/b']), ['a', '…/b']); + assert.deepStrictEqual(labels.shorten(['a/b', 'a/b/c']), ['…/b', '…/c']); + assert.deepStrictEqual(labels.shorten(['a', 'a/b', 'a/b/c']), ['a', '…/b', '…/c']); + assert.deepStrictEqual(labels.shorten(['/a/b', '/a/c']), ['/a/b', '/a/c']); // same ending - assert.deepEqual(labels.shorten(['a', 'b/a']), ['a', 'b/…']); - assert.deepEqual(labels.shorten(['a/b/c', 'd/b/c']), ['a/…', 'd/…']); - assert.deepEqual(labels.shorten(['a/b/c/d', 'f/b/c/d']), ['a/…', 'f/…']); - assert.deepEqual(labels.shorten(['d/e/a/b/c', 'd/b/c']), ['…/a/…', 'd/b/…']); - assert.deepEqual(labels.shorten(['a/b/c/d', 'a/f/b/c/d']), ['a/b/…', '…/f/…']); - assert.deepEqual(labels.shorten(['a/b/a', 'b/b/a']), ['a/b/…', 'b/b/…']); - assert.deepEqual(labels.shorten(['d/f/a/b/c', 'h/d/b/c']), ['…/a/…', 'h/…']); - assert.deepEqual(labels.shorten(['/x/b', '/y/b']), ['/x/…', '/y/…']); + assert.deepStrictEqual(labels.shorten(['a', 'b/a']), ['a', 'b/…']); + assert.deepStrictEqual(labels.shorten(['a/b/c', 'd/b/c']), ['a/…', 'd/…']); + assert.deepStrictEqual(labels.shorten(['a/b/c/d', 'f/b/c/d']), ['a/…', 'f/…']); + assert.deepStrictEqual(labels.shorten(['d/e/a/b/c', 'd/b/c']), ['…/a/…', 'd/b/…']); + assert.deepStrictEqual(labels.shorten(['a/b/c/d', 'a/f/b/c/d']), ['a/b/…', '…/f/…']); + assert.deepStrictEqual(labels.shorten(['a/b/a', 'b/b/a']), ['a/b/…', 'b/b/…']); + assert.deepStrictEqual(labels.shorten(['d/f/a/b/c', 'h/d/b/c']), ['…/a/…', 'h/…']); + assert.deepStrictEqual(labels.shorten(['/x/b', '/y/b']), ['/x/…', '/y/…']); // same name ending - assert.deepEqual(labels.shorten(['a/b', 'a/c', 'a/e-b']), ['…/b', '…/c', '…/e-b']); + assert.deepStrictEqual(labels.shorten(['a/b', 'a/c', 'a/e-b']), ['…/b', '…/c', '…/e-b']); // same in the middle - assert.deepEqual(labels.shorten(['a/b/c', 'd/b/e']), ['…/c', '…/e']); + assert.deepStrictEqual(labels.shorten(['a/b/c', 'd/b/e']), ['…/c', '…/e']); // case-sensitive - assert.deepEqual(labels.shorten(['a/b/c', 'd/b/C']), ['…/c', '…/C']); + assert.deepStrictEqual(labels.shorten(['a/b/c', 'd/b/C']), ['…/c', '…/C']); // empty or null - assert.deepEqual(labels.shorten(['', null!]), ['./', null]); + assert.deepStrictEqual(labels.shorten(['', null!]), ['./', null]); - assert.deepEqual(labels.shorten(['a', 'a/b', 'a/b/c', 'd/b/c', 'd/b']), ['a', 'a/b', 'a/b/c', 'd/b/c', 'd/b']); - assert.deepEqual(labels.shorten(['a', 'a/b', 'b']), ['a', 'a/b', 'b']); - assert.deepEqual(labels.shorten(['', 'a', 'b', 'b/c', 'a/c']), ['./', 'a', 'b', 'b/c', 'a/c']); + assert.deepStrictEqual(labels.shorten(['a', 'a/b', 'a/b/c', 'd/b/c', 'd/b']), ['a', 'a/b', 'a/b/c', 'd/b/c', 'd/b']); + assert.deepStrictEqual(labels.shorten(['a', 'a/b', 'b']), ['a', 'a/b', 'b']); + assert.deepStrictEqual(labels.shorten(['', 'a', 'b', 'b/c', 'a/c']), ['./', 'a', 'b', 'b/c', 'a/c']); }); test('template', () => { @@ -142,41 +134,32 @@ suite('Labels', () => { assert.strictEqual(labels.template(t, { dirty: '* ', activeEditorShort: 'somefile.txt', rootName: 'monaco', appName: 'Visual Studio Code', separator: { label: ' - ' } }), '* somefile.txt - monaco - Visual Studio Code'); }); - test('getBaseLabel - unix', () => { - if (platform.isWindows) { - assert.ok(true); - return; - } - - assert.equal(labels.getBaseLabel('/some/folder/file.txt'), 'file.txt'); - assert.equal(labels.getBaseLabel('/some/folder'), 'folder'); - assert.equal(labels.getBaseLabel('/'), '/'); + (platform.isWindows ? test.skip : test)('getBaseLabel - unix', () => { + assert.strictEqual(labels.getBaseLabel('/some/folder/file.txt'), 'file.txt'); + assert.strictEqual(labels.getBaseLabel('/some/folder'), 'folder'); + assert.strictEqual(labels.getBaseLabel('/'), '/'); }); - test('getBaseLabel - windows', () => { - if (!platform.isWindows) { - assert.ok(true); - return; - } - - assert.equal(labels.getBaseLabel('c:'), 'C:'); - assert.equal(labels.getBaseLabel('c:\\'), 'C:'); - assert.equal(labels.getBaseLabel('c:\\some\\folder\\file.txt'), 'file.txt'); - assert.equal(labels.getBaseLabel('c:\\some\\folder'), 'folder'); + (!platform.isWindows ? test.skip : test)('getBaseLabel - windows', () => { + assert.strictEqual(labels.getBaseLabel('c:'), 'C:'); + assert.strictEqual(labels.getBaseLabel('c:\\'), 'C:'); + assert.strictEqual(labels.getBaseLabel('c:\\some\\folder\\file.txt'), 'file.txt'); + assert.strictEqual(labels.getBaseLabel('c:\\some\\folder'), 'folder'); + assert.strictEqual(labels.getBaseLabel('c:\\some\\f:older'), 'f:older'); // https://github.com/microsoft/vscode-remote-release/issues/4227 }); test('mnemonicButtonLabel', () => { - assert.equal(labels.mnemonicButtonLabel('Hello World'), 'Hello World'); - assert.equal(labels.mnemonicButtonLabel(''), ''); + assert.strictEqual(labels.mnemonicButtonLabel('Hello World'), 'Hello World'); + assert.strictEqual(labels.mnemonicButtonLabel(''), ''); if (platform.isWindows) { - assert.equal(labels.mnemonicButtonLabel('Hello & World'), 'Hello && World'); - assert.equal(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do ¬ Save && Continue'); + assert.strictEqual(labels.mnemonicButtonLabel('Hello & World'), 'Hello && World'); + assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do ¬ Save && Continue'); } else if (platform.isMacintosh) { - assert.equal(labels.mnemonicButtonLabel('Hello & World'), 'Hello & World'); - assert.equal(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do not Save & Continue'); + assert.strictEqual(labels.mnemonicButtonLabel('Hello & World'), 'Hello & World'); + assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do not Save & Continue'); } else { - assert.equal(labels.mnemonicButtonLabel('Hello & World'), 'Hello & World'); - assert.equal(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do _not Save & Continue'); + assert.strictEqual(labels.mnemonicButtonLabel('Hello & World'), 'Hello & World'); + assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do _not Save & Continue'); } }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/common/linkedList.test.ts b/src/vs/base/test/common/linkedList.test.ts index dbb057a21..d4eb08320 100644 --- a/src/vs/base/test/common/linkedList.test.ts +++ b/src/vs/base/test/common/linkedList.test.ts @@ -11,19 +11,19 @@ suite('LinkedList', function () { function assertElements(list: LinkedList, ...elements: E[]) { // check size - assert.equal(list.size, elements.length); + assert.strictEqual(list.size, elements.length); // assert toArray - assert.deepEqual(Array.from(list), elements); + assert.deepStrictEqual(Array.from(list), elements); // assert Symbol.iterator (1) - assert.deepEqual([...list], elements); + assert.deepStrictEqual([...list], elements); // assert Symbol.iterator (2) for (const item of list) { - assert.equal(item, elements.shift()); + assert.strictEqual(item, elements.shift()); } - assert.equal(elements.length, 0); + assert.strictEqual(elements.length, 0); } test('Push/Iter', () => { @@ -123,14 +123,14 @@ suite('LinkedList', function () { assertElements(list, 'a', 'b'); let a = list.shift(); - assert.equal(a, 'a'); + assert.strictEqual(a, 'a'); assertElements(list, 'b'); list.unshift('a'); assertElements(list, 'a', 'b'); let b = list.pop(); - assert.equal(b, 'b'); + assert.strictEqual(b, 'b'); assertElements(list, 'a'); }); diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index 5781de52a..8988e8e68 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -16,8 +16,8 @@ suite('Map', () => { map.set('bk', 'bv'); assert.deepStrictEqual([...map.keys()], ['ak', 'bk']); assert.deepStrictEqual([...map.values()], ['av', 'bv']); - assert.equal(map.first, 'av'); - assert.equal(map.last, 'bv'); + assert.strictEqual(map.first, 'av'); + assert.strictEqual(map.last, 'bv'); }); test('LinkedMap - Touch Old one', () => { @@ -77,7 +77,7 @@ suite('Map', () => { test('LinkedMap - basics', function () { const map = new LinkedMap(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); map.set('1', 1); map.set('2', '2'); @@ -89,23 +89,23 @@ suite('Map', () => { const date = Date.now(); map.set('5', date); - assert.equal(map.size, 5); - assert.equal(map.get('1'), 1); - assert.equal(map.get('2'), '2'); - assert.equal(map.get('3'), true); - assert.equal(map.get('4'), obj); - assert.equal(map.get('5'), date); + assert.strictEqual(map.size, 5); + assert.strictEqual(map.get('1'), 1); + assert.strictEqual(map.get('2'), '2'); + assert.strictEqual(map.get('3'), true); + assert.strictEqual(map.get('4'), obj); + assert.strictEqual(map.get('5'), date); assert.ok(!map.get('6')); map.delete('6'); - assert.equal(map.size, 5); - assert.equal(map.delete('1'), true); - assert.equal(map.delete('2'), true); - assert.equal(map.delete('3'), true); - assert.equal(map.delete('4'), true); - assert.equal(map.delete('5'), true); + assert.strictEqual(map.size, 5); + assert.strictEqual(map.delete('1'), true); + assert.strictEqual(map.delete('2'), true); + assert.strictEqual(map.delete('3'), true); + assert.strictEqual(map.delete('4'), true); + assert.strictEqual(map.delete('5'), true); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); assert.ok(!map.get('5')); assert.ok(!map.get('4')); assert.ok(!map.get('3')); @@ -117,13 +117,13 @@ suite('Map', () => { map.set('3', true); assert.ok(map.has('1')); - assert.equal(map.get('1'), 1); - assert.equal(map.get('2'), '2'); - assert.equal(map.get('3'), true); + assert.strictEqual(map.get('1'), 1); + assert.strictEqual(map.get('2'), '2'); + assert.strictEqual(map.get('3'), true); map.clear(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); assert.ok(!map.get('1')); assert.ok(!map.get('2')); assert.ok(!map.get('3')); @@ -231,7 +231,7 @@ suite('Map', () => { for (let i = 11; i <= 20; i++) { cache.set(i, i); } - assert.deepEqual(cache.size, 15); + assert.deepStrictEqual(cache.size, 15); let values: number[] = []; for (let i = 6; i <= 20; i++) { values.push(cache.get(i)!); @@ -269,14 +269,14 @@ suite('Map', () => { let i = 0; map.forEach((value, key) => { if (i === 0) { - assert.equal(key, 'ak'); - assert.equal(value, 'av'); + assert.strictEqual(key, 'ak'); + assert.strictEqual(value, 'av'); } else if (i === 1) { - assert.equal(key, 'bk'); - assert.equal(value, 'bv'); + assert.strictEqual(key, 'bk'); + assert.strictEqual(value, 'bv'); } else if (i === 2) { - assert.equal(key, 'ck'); - assert.equal(value, 'cv'); + assert.strictEqual(key, 'ck'); + assert.strictEqual(value, 'cv'); } i++; }); @@ -285,44 +285,44 @@ suite('Map', () => { test('LinkedMap - delete Head and Tail', function () { const map = new LinkedMap(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); map.set('1', 1); - assert.equal(map.size, 1); + assert.strictEqual(map.size, 1); map.delete('1'); - assert.equal(map.get('1'), undefined); - assert.equal(map.size, 0); - assert.equal([...map.keys()].length, 0); + assert.strictEqual(map.get('1'), undefined); + assert.strictEqual(map.size, 0); + assert.strictEqual([...map.keys()].length, 0); }); test('LinkedMap - delete Head', function () { const map = new LinkedMap(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); map.set('1', 1); map.set('2', 2); - assert.equal(map.size, 2); + assert.strictEqual(map.size, 2); map.delete('1'); - assert.equal(map.get('2'), 2); - assert.equal(map.size, 1); - assert.equal([...map.keys()].length, 1); - assert.equal([...map.keys()][0], 2); + assert.strictEqual(map.get('2'), 2); + assert.strictEqual(map.size, 1); + assert.strictEqual([...map.keys()].length, 1); + assert.strictEqual([...map.keys()][0], '2'); }); test('LinkedMap - delete Tail', function () { const map = new LinkedMap(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); map.set('1', 1); map.set('2', 2); - assert.equal(map.size, 2); + assert.strictEqual(map.size, 2); map.delete('2'); - assert.equal(map.get('1'), 1); - assert.equal(map.size, 1); - assert.equal([...map.keys()].length, 1); - assert.equal([...map.keys()][0], 1); + assert.strictEqual(map.get('1'), 1); + assert.strictEqual(map.size, 1); + assert.strictEqual([...map.keys()].length, 1); + assert.strictEqual([...map.keys()][0], '1'); }); @@ -330,100 +330,100 @@ suite('Map', () => { const iter = new PathIterator(); iter.reset('file:///usr/bin/file.txt'); - assert.equal(iter.value(), 'file:'); - assert.equal(iter.hasNext(), true); - assert.equal(iter.cmp('file:'), 0); + assert.strictEqual(iter.value(), 'file:'); + assert.strictEqual(iter.hasNext(), true); + assert.strictEqual(iter.cmp('file:'), 0); assert.ok(iter.cmp('a') < 0); assert.ok(iter.cmp('aile:') < 0); assert.ok(iter.cmp('z') > 0); assert.ok(iter.cmp('zile:') > 0); iter.next(); - assert.equal(iter.value(), 'usr'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'usr'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'bin'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'bin'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'file.txt'); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), 'file.txt'); + assert.strictEqual(iter.hasNext(), false); iter.next(); - assert.equal(iter.value(), ''); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), ''); + assert.strictEqual(iter.hasNext(), false); iter.next(); - assert.equal(iter.value(), ''); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), ''); + assert.strictEqual(iter.hasNext(), false); // iter.reset('/foo/bar/'); - assert.equal(iter.value(), 'foo'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'foo'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'bar'); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), 'bar'); + assert.strictEqual(iter.hasNext(), false); }); test('URIIterator', function () { const iter = new UriIterator(() => false); iter.reset(URI.parse('file:///usr/bin/file.txt')); - assert.equal(iter.value(), 'file'); - // assert.equal(iter.cmp('FILE'), 0); - assert.equal(iter.cmp('file'), 0); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'file'); + // assert.strictEqual(iter.cmp('FILE'), 0); + assert.strictEqual(iter.cmp('file'), 0); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'usr'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'usr'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'bin'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'bin'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'file.txt'); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), 'file.txt'); + assert.strictEqual(iter.hasNext(), false); iter.reset(URI.parse('file://share/usr/bin/file.txt?foo')); // scheme - assert.equal(iter.value(), 'file'); - // assert.equal(iter.cmp('FILE'), 0); - assert.equal(iter.cmp('file'), 0); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'file'); + // assert.strictEqual(iter.cmp('FILE'), 0); + assert.strictEqual(iter.cmp('file'), 0); + assert.strictEqual(iter.hasNext(), true); iter.next(); // authority - assert.equal(iter.value(), 'share'); - assert.equal(iter.cmp('SHARe'), 0); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'share'); + assert.strictEqual(iter.cmp('SHARe'), 0); + assert.strictEqual(iter.hasNext(), true); iter.next(); // path - assert.equal(iter.value(), 'usr'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'usr'); + assert.strictEqual(iter.hasNext(), true); iter.next(); // path - assert.equal(iter.value(), 'bin'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'bin'); + assert.strictEqual(iter.hasNext(), true); iter.next(); // path - assert.equal(iter.value(), 'file.txt'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'file.txt'); + assert.strictEqual(iter.hasNext(), true); iter.next(); // query - assert.equal(iter.value(), 'foo'); - assert.equal(iter.cmp('z') > 0, true); - assert.equal(iter.cmp('a') < 0, true); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), 'foo'); + assert.strictEqual(iter.cmp('z') > 0, true); + assert.strictEqual(iter.cmp('a') < 0, true); + assert.strictEqual(iter.hasNext(), false); }); function assertTernarySearchTree(trie: TernarySearchTree, ...elements: [string, E][]) { @@ -432,24 +432,24 @@ suite('Map', () => { map.set(key, value); } map.forEach((value, key) => { - assert.equal(trie.get(key), value); + assert.strictEqual(trie.get(key), value); }); // forEach let forEachCount = 0; trie.forEach((element, key) => { - assert.equal(element, map.get(key)); + assert.strictEqual(element, map.get(key)); forEachCount++; }); - assert.equal(map.size, forEachCount); + assert.strictEqual(map.size, forEachCount); // iterator let iterCount = 0; for (let [key, value] of trie) { - assert.equal(value, map.get(key)); + assert.strictEqual(value, map.get(key)); iterCount++; } - assert.equal(map.size, iterCount); + assert.strictEqual(map.size, iterCount); } test('TernarySearchTree - set', function () { @@ -493,13 +493,13 @@ suite('Map', () => { trie.set('foobar', 2); trie.set('foobaz', 3); - assert.equal(trie.findSubstr('f'), undefined); - assert.equal(trie.findSubstr('z'), undefined); - assert.equal(trie.findSubstr('foo'), 1); - assert.equal(trie.findSubstr('fooö'), 1); - assert.equal(trie.findSubstr('fooba'), 1); - assert.equal(trie.findSubstr('foobarr'), 2); - assert.equal(trie.findSubstr('foobazrr'), 3); + assert.strictEqual(trie.findSubstr('f'), undefined); + assert.strictEqual(trie.findSubstr('z'), undefined); + assert.strictEqual(trie.findSubstr('foo'), 1); + assert.strictEqual(trie.findSubstr('fooö'), 1); + assert.strictEqual(trie.findSubstr('fooba'), 1); + assert.strictEqual(trie.findSubstr('foobarr'), 2); + assert.strictEqual(trie.findSubstr('foobazrr'), 3); }); test('TernarySearchTree - basics', function () { @@ -509,27 +509,27 @@ suite('Map', () => { trie.set('bar', 2); trie.set('foobar', 3); - assert.equal(trie.get('foo'), 1); - assert.equal(trie.get('bar'), 2); - assert.equal(trie.get('foobar'), 3); - assert.equal(trie.get('foobaz'), undefined); - assert.equal(trie.get('foobarr'), undefined); + assert.strictEqual(trie.get('foo'), 1); + assert.strictEqual(trie.get('bar'), 2); + assert.strictEqual(trie.get('foobar'), 3); + assert.strictEqual(trie.get('foobaz'), undefined); + assert.strictEqual(trie.get('foobarr'), undefined); - assert.equal(trie.findSubstr('fo'), undefined); - assert.equal(trie.findSubstr('foo'), 1); - assert.equal(trie.findSubstr('foooo'), 1); + assert.strictEqual(trie.findSubstr('fo'), undefined); + assert.strictEqual(trie.findSubstr('foo'), 1); + assert.strictEqual(trie.findSubstr('foooo'), 1); trie.delete('foobar'); trie.delete('bar'); - assert.equal(trie.get('foobar'), undefined); - assert.equal(trie.get('bar'), undefined); + assert.strictEqual(trie.get('foobar'), undefined); + assert.strictEqual(trie.get('bar'), undefined); trie.set('foobar', 17); trie.set('barr', 18); - assert.equal(trie.get('foobar'), 17); - assert.equal(trie.get('barr'), 18); - assert.equal(trie.get('bar'), undefined); + assert.strictEqual(trie.get('foobar'), 17); + assert.strictEqual(trie.get('barr'), 18); + assert.strictEqual(trie.get('bar'), undefined); }); test('TernarySearchTree - delete & cleanup', function () { @@ -576,20 +576,20 @@ suite('Map', () => { trie.set('/user/foo', 2); trie.set('/user/foo/flip/flop', 3); - assert.equal(trie.get('/user/foo/bar'), 1); - assert.equal(trie.get('/user/foo'), 2); - assert.equal(trie.get('/user//foo'), 2); - assert.equal(trie.get('/user\\foo'), 2); - assert.equal(trie.get('/user/foo/flip/flop'), 3); + assert.strictEqual(trie.get('/user/foo/bar'), 1); + assert.strictEqual(trie.get('/user/foo'), 2); + assert.strictEqual(trie.get('/user//foo'), 2); + assert.strictEqual(trie.get('/user\\foo'), 2); + assert.strictEqual(trie.get('/user/foo/flip/flop'), 3); - assert.equal(trie.findSubstr('/user/bar'), undefined); - assert.equal(trie.findSubstr('/user/foo'), 2); - assert.equal(trie.findSubstr('\\user\\foo'), 2); - assert.equal(trie.findSubstr('/user//foo'), 2); - assert.equal(trie.findSubstr('/user/foo/ba'), 2); - assert.equal(trie.findSubstr('/user/foo/far/boo'), 2); - assert.equal(trie.findSubstr('/user/foo/bar'), 1); - assert.equal(trie.findSubstr('/user/foo/bar/far/boo'), 1); + assert.strictEqual(trie.findSubstr('/user/bar'), undefined); + assert.strictEqual(trie.findSubstr('/user/foo'), 2); + assert.strictEqual(trie.findSubstr('\\user\\foo'), 2); + assert.strictEqual(trie.findSubstr('/user//foo'), 2); + assert.strictEqual(trie.findSubstr('/user/foo/ba'), 2); + assert.strictEqual(trie.findSubstr('/user/foo/far/boo'), 2); + assert.strictEqual(trie.findSubstr('/user/foo/bar'), 1); + assert.strictEqual(trie.findSubstr('/user/foo/bar/far/boo'), 1); }); test('TernarySearchTree (PathSegments) - lookup', function () { @@ -599,11 +599,11 @@ suite('Map', () => { map.set('/user/foo', 2); map.set('/user/foo/flip/flop', 3); - assert.equal(map.get('/foo'), undefined); - assert.equal(map.get('/user'), undefined); - assert.equal(map.get('/user/foo'), 2); - assert.equal(map.get('/user/foo/bar'), 1); - assert.equal(map.get('/user/foo/bar/boo'), undefined); + assert.strictEqual(map.get('/foo'), undefined); + assert.strictEqual(map.get('/user'), undefined); + assert.strictEqual(map.get('/user/foo'), 2); + assert.strictEqual(map.get('/user/foo/bar'), 1); + assert.strictEqual(map.get('/user/foo/bar/boo'), undefined); }); test('TernarySearchTree (PathSegments) - superstr', function () { @@ -618,31 +618,31 @@ suite('Map', () => { let iter = map.findSuperstr('/user'); item = iter!.next(); - assert.equal(item.value[1], 2); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 2); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value[1], 1); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 1); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value[1], 3); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 3); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); iter = map.findSuperstr('/usr'); item = iter!.next(); - assert.equal(item.value[1], 4); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 4); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); - assert.equal(map.findSuperstr('/not'), undefined); - assert.equal(map.findSuperstr('/us'), undefined); - assert.equal(map.findSuperstr('/usrr'), undefined); - assert.equal(map.findSuperstr('/userr'), undefined); + assert.strictEqual(map.findSuperstr('/not'), undefined); + assert.strictEqual(map.findSuperstr('/us'), undefined); + assert.strictEqual(map.findSuperstr('/usrr'), undefined); + assert.strictEqual(map.findSuperstr('/userr'), undefined); }); @@ -688,16 +688,16 @@ suite('Map', () => { trie.set(URI.file('/user/foo'), 2); trie.set(URI.file('/user/foo/flip/flop'), 3); - assert.equal(trie.get(URI.file('/user/foo/bar')), 1); - assert.equal(trie.get(URI.file('/user/foo')), 2); - assert.equal(trie.get(URI.file('/user/foo/flip/flop')), 3); + assert.strictEqual(trie.get(URI.file('/user/foo/bar')), 1); + assert.strictEqual(trie.get(URI.file('/user/foo')), 2); + assert.strictEqual(trie.get(URI.file('/user/foo/flip/flop')), 3); - assert.equal(trie.findSubstr(URI.file('/user/bar')), undefined); - assert.equal(trie.findSubstr(URI.file('/user/foo')), 2); - assert.equal(trie.findSubstr(URI.file('/user/foo/ba')), 2); - assert.equal(trie.findSubstr(URI.file('/user/foo/far/boo')), 2); - assert.equal(trie.findSubstr(URI.file('/user/foo/bar')), 1); - assert.equal(trie.findSubstr(URI.file('/user/foo/bar/far/boo')), 1); + assert.strictEqual(trie.findSubstr(URI.file('/user/bar')), undefined); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo')), 2); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo/ba')), 2); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo/far/boo')), 2); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo/bar')), 1); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo/bar/far/boo')), 1); }); test('TernarySearchTree (URI) - lookup', function () { @@ -708,23 +708,23 @@ suite('Map', () => { map.set(URI.parse('http://foo.bar/user/foo?QUERY'), 3); map.set(URI.parse('http://foo.bar/user/foo/flip/flop'), 3); - assert.equal(map.get(URI.parse('http://foo.bar/foo')), undefined); - assert.equal(map.get(URI.parse('http://foo.bar/user')), undefined); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo/bar')), 1); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo?query')), 2); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo?Query')), undefined); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo?QUERY')), 3); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo/bar/boo')), undefined); + assert.strictEqual(map.get(URI.parse('http://foo.bar/foo')), undefined); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user')), undefined); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo/bar')), 1); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo?query')), 2); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo?Query')), undefined); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo?QUERY')), 3); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo/bar/boo')), undefined); }); test('TernarySearchTree (URI) - lookup, casing', function () { const map = new TernarySearchTree(new UriIterator(uri => /^https?$/.test(uri.scheme))); map.set(URI.parse('http://foo.bar/user/foo/bar'), 1); - assert.equal(map.get(URI.parse('http://foo.bar/USER/foo/bar')), 1); + assert.strictEqual(map.get(URI.parse('http://foo.bar/USER/foo/bar')), 1); map.set(URI.parse('foo://foo.bar/user/foo/bar'), 1); - assert.equal(map.get(URI.parse('foo://foo.bar/USER/foo/bar')), undefined); + assert.strictEqual(map.get(URI.parse('foo://foo.bar/USER/foo/bar')), undefined); }); test('TernarySearchTree (URI) - superstr', function () { @@ -739,48 +739,48 @@ suite('Map', () => { let iter = map.findSuperstr(URI.file('/user'))!; item = iter.next(); - assert.equal(item.value[1], 2); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 2); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 1); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 1); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 3); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 3); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); iter = map.findSuperstr(URI.file('/usr'))!; item = iter.next(); - assert.equal(item.value[1], 4); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 4); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); iter = map.findSuperstr(URI.file('/'))!; item = iter.next(); - assert.equal(item.value[1], 2); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 2); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 1); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 1); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 3); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 3); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 4); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 4); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); - assert.equal(map.findSuperstr(URI.file('/not')), undefined); - assert.equal(map.findSuperstr(URI.file('/us')), undefined); - assert.equal(map.findSuperstr(URI.file('/usrr')), undefined); - assert.equal(map.findSuperstr(URI.file('/userr')), undefined); + assert.strictEqual(map.findSuperstr(URI.file('/not')), undefined); + assert.strictEqual(map.findSuperstr(URI.file('/us')), undefined); + assert.strictEqual(map.findSuperstr(URI.file('/usrr')), undefined); + assert.strictEqual(map.findSuperstr(URI.file('/userr')), undefined); }); test('TernarySearchTree (ConfigKeySegments) - basics', function () { @@ -790,16 +790,16 @@ suite('Map', () => { trie.set('config.foo', 2); trie.set('config.foo.flip.flop', 3); - assert.equal(trie.get('config.foo.bar'), 1); - assert.equal(trie.get('config.foo'), 2); - assert.equal(trie.get('config.foo.flip.flop'), 3); + assert.strictEqual(trie.get('config.foo.bar'), 1); + assert.strictEqual(trie.get('config.foo'), 2); + assert.strictEqual(trie.get('config.foo.flip.flop'), 3); - assert.equal(trie.findSubstr('config.bar'), undefined); - assert.equal(trie.findSubstr('config.foo'), 2); - assert.equal(trie.findSubstr('config.foo.ba'), 2); - assert.equal(trie.findSubstr('config.foo.far.boo'), 2); - assert.equal(trie.findSubstr('config.foo.bar'), 1); - assert.equal(trie.findSubstr('config.foo.bar.far.boo'), 1); + assert.strictEqual(trie.findSubstr('config.bar'), undefined); + assert.strictEqual(trie.findSubstr('config.foo'), 2); + assert.strictEqual(trie.findSubstr('config.foo.ba'), 2); + assert.strictEqual(trie.findSubstr('config.foo.far.boo'), 2); + assert.strictEqual(trie.findSubstr('config.foo.bar'), 1); + assert.strictEqual(trie.findSubstr('config.foo.bar.far.boo'), 1); }); test('TernarySearchTree (ConfigKeySegments) - lookup', function () { @@ -809,11 +809,11 @@ suite('Map', () => { map.set('config.foo', 2); map.set('config.foo.flip.flop', 3); - assert.equal(map.get('foo'), undefined); - assert.equal(map.get('config'), undefined); - assert.equal(map.get('config.foo'), 2); - assert.equal(map.get('config.foo.bar'), 1); - assert.equal(map.get('config.foo.bar.boo'), undefined); + assert.strictEqual(map.get('foo'), undefined); + assert.strictEqual(map.get('config'), undefined); + assert.strictEqual(map.get('config.foo'), 2); + assert.strictEqual(map.get('config.foo.bar'), 1); + assert.strictEqual(map.get('config.foo.bar.boo'), undefined); }); test('TernarySearchTree (ConfigKeySegments) - superstr', function () { @@ -828,21 +828,21 @@ suite('Map', () => { let iter = map.findSuperstr('config'); item = iter!.next(); - assert.equal(item.value[1], 2); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 2); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value[1], 1); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 1); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value[1], 3); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 3); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); - assert.equal(map.findSuperstr('foo'), undefined); - assert.equal(map.findSuperstr('config.foo.no'), undefined); - assert.equal(map.findSuperstr('config.foop'), undefined); + assert.strictEqual(map.findSuperstr('foo'), undefined); + assert.strictEqual(map.findSuperstr('config.foo.no'), undefined); + assert.strictEqual(map.findSuperstr('config.foop'), undefined); }); @@ -891,7 +891,7 @@ suite('Map', () => { const resource5 = URI.parse('some://5'); const resource6 = URI.parse('some://6'); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); let res = map.set(resource1, 1); assert.ok(res === map); @@ -899,13 +899,13 @@ suite('Map', () => { map.set(resource3, true); const values = [...map.values()]; - assert.equal(values[0], 1); - assert.equal(values[1], '2'); - assert.equal(values[2], true); + assert.strictEqual(values[0], 1); + assert.strictEqual(values[1], '2'); + assert.strictEqual(values[2], true); let counter = 0; map.forEach((value, key, mapObj) => { - assert.equal(value, values[counter++]); + assert.strictEqual(value, values[counter++]); assert.ok(URI.isUri(key)); assert.ok(map === mapObj); }); @@ -916,23 +916,23 @@ suite('Map', () => { const date = Date.now(); map.set(resource5, date); - assert.equal(map.size, 5); - assert.equal(map.get(resource1), 1); - assert.equal(map.get(resource2), '2'); - assert.equal(map.get(resource3), true); - assert.equal(map.get(resource4), obj); - assert.equal(map.get(resource5), date); + assert.strictEqual(map.size, 5); + assert.strictEqual(map.get(resource1), 1); + assert.strictEqual(map.get(resource2), '2'); + assert.strictEqual(map.get(resource3), true); + assert.strictEqual(map.get(resource4), obj); + assert.strictEqual(map.get(resource5), date); assert.ok(!map.get(resource6)); map.delete(resource6); - assert.equal(map.size, 5); + assert.strictEqual(map.size, 5); assert.ok(map.delete(resource1)); assert.ok(map.delete(resource2)); assert.ok(map.delete(resource3)); assert.ok(map.delete(resource4)); assert.ok(map.delete(resource5)); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); assert.ok(!map.get(resource5)); assert.ok(!map.get(resource4)); assert.ok(!map.get(resource3)); @@ -944,13 +944,13 @@ suite('Map', () => { map.set(resource3, true); assert.ok(map.has(resource1)); - assert.equal(map.get(resource1), 1); - assert.equal(map.get(resource2), '2'); - assert.equal(map.get(resource3), true); + assert.strictEqual(map.get(resource1), 1); + assert.strictEqual(map.get(resource2), '2'); + assert.strictEqual(map.get(resource3), true); map.clear(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); assert.ok(!map.get(resource1)); assert.ok(!map.get(resource2)); assert.ok(!map.get(resource3)); @@ -971,16 +971,16 @@ suite('Map', () => { const fileAUpper = URI.parse('file://SOME/FILEA'); map.set(fileA, 'true'); - assert.equal(map.get(fileA), 'true'); + assert.strictEqual(map.get(fileA), 'true'); assert.ok(!map.get(fileAUpper)); assert.ok(!map.get(fileB)); map.set(fileAUpper, 'false'); - assert.equal(map.get(fileAUpper), 'false'); + assert.strictEqual(map.get(fileAUpper), 'false'); - assert.equal(map.get(fileA), 'true'); + assert.strictEqual(map.get(fileA), 'true'); const windowsFile = URI.file('c:\\test with %25\\c#code'); const uncFile = URI.file('\\\\shäres\\path\\c#\\plugin.json'); @@ -988,8 +988,8 @@ suite('Map', () => { map.set(windowsFile, 'true'); map.set(uncFile, 'true'); - assert.equal(map.get(windowsFile), 'true'); - assert.equal(map.get(uncFile), 'true'); + assert.strictEqual(map.get(windowsFile), 'true'); + assert.strictEqual(map.get(uncFile), 'true'); }); test('ResourceMap - files (ignorecase)', function () { @@ -1000,16 +1000,16 @@ suite('Map', () => { const fileAUpper = URI.parse('file://SOME/FILEA'); map.set(fileA, 'true'); - assert.equal(map.get(fileA), 'true'); + assert.strictEqual(map.get(fileA), 'true'); - assert.equal(map.get(fileAUpper), 'true'); + assert.strictEqual(map.get(fileAUpper), 'true'); assert.ok(!map.get(fileB)); map.set(fileAUpper, 'false'); - assert.equal(map.get(fileAUpper), 'false'); + assert.strictEqual(map.get(fileAUpper), 'false'); - assert.equal(map.get(fileA), 'false'); + assert.strictEqual(map.get(fileA), 'false'); const windowsFile = URI.file('c:\\test with %25\\c#code'); const uncFile = URI.file('\\\\shäres\\path\\c#\\plugin.json'); @@ -1017,7 +1017,7 @@ suite('Map', () => { map.set(windowsFile, 'true'); map.set(uncFile, 'true'); - assert.equal(map.get(windowsFile), 'true'); - assert.equal(map.get(uncFile), 'true'); + assert.strictEqual(map.get(windowsFile), 'true'); + assert.strictEqual(map.get(uncFile), 'true'); }); }); diff --git a/src/vs/base/test/common/markdownString.test.ts b/src/vs/base/test/common/markdownString.test.ts index 424c2bd46..7d9d64f2b 100644 --- a/src/vs/base/test/common/markdownString.test.ts +++ b/src/vs/base/test/common/markdownString.test.ts @@ -11,13 +11,13 @@ suite('MarkdownString', () => { test('Escape leading whitespace', function () { const mds = new MarkdownString(); mds.appendText('Hello\n Not a code block'); - assert.equal(mds.value, 'Hello\n\n    Not a code block'); + assert.strictEqual(mds.value, 'Hello\n\n    Not a code block'); }); test('MarkdownString.appendText doesn\'t escape quote #109040', function () { const mds = new MarkdownString(); mds.appendText('> Text\n>More'); - assert.equal(mds.value, '\\> Text\n\n\\>More'); + assert.strictEqual(mds.value, '\\> Text\n\n\\>More'); }); test('appendText', () => { @@ -25,7 +25,7 @@ suite('MarkdownString', () => { const mds = new MarkdownString(); mds.appendText('# foo\n*bar*'); - assert.equal(mds.value, '\\# foo\n\n\\*bar\\*'); + assert.strictEqual(mds.value, '\\# foo\n\n\\*bar\\*'); }); suite('ThemeIcons', () => { @@ -36,21 +36,21 @@ suite('MarkdownString', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendText('$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '\\\\$\\(zap\\) $\\(not a theme icon\\) \\\\$\\(add\\)'); + assert.strictEqual(mds.value, '\\\\$\\(zap\\) $\\(not a theme icon\\) \\\\$\\(add\\)'); }); test('appendMarkdown', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendMarkdown('$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '$(zap) $(not a theme icon) $(add)'); + assert.strictEqual(mds.value, '$(zap) $(not a theme icon) $(add)'); }); test('appendMarkdown with escaped icon', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendMarkdown('\\$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '\\$(zap) $(not a theme icon) $(add)'); + assert.strictEqual(mds.value, '\\$(zap) $(not a theme icon) $(add)'); }); }); @@ -61,21 +61,21 @@ suite('MarkdownString', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: false }); mds.appendText('$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '$\\(zap\\) $\\(not a theme icon\\) $\\(add\\)'); + assert.strictEqual(mds.value, '$\\(zap\\) $\\(not a theme icon\\) $\\(add\\)'); }); test('appendMarkdown', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: false }); mds.appendMarkdown('$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '$(zap) $(not a theme icon) $(add)'); + assert.strictEqual(mds.value, '$(zap) $(not a theme icon) $(add)'); }); test('appendMarkdown with escaped icon', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendMarkdown('\\$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '\\$(zap) $(not a theme icon) $(add)'); + assert.strictEqual(mds.value, '\\$(zap) $(not a theme icon) $(add)'); }); }); diff --git a/src/vs/base/test/common/mime.test.ts b/src/vs/base/test/common/mime.test.ts index a7ac5a177..f2583fed4 100644 --- a/src/vs/base/test/common/mime.test.ts +++ b/src/vs/base/test/common/mime.test.ts @@ -11,38 +11,38 @@ suite('Mime', () => { test('Dynamically Register Text Mime', () => { let guess = guessMimeTypes(URI.file('foo.monaco')); - assert.deepEqual(guess, ['application/unknown']); + assert.deepStrictEqual(guess, ['application/unknown']); registerTextMime({ id: 'monaco', extension: '.monaco', mime: 'text/monaco' }); guess = guessMimeTypes(URI.file('foo.monaco')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); guess = guessMimeTypes(URI.file('.monaco')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); registerTextMime({ id: 'codefile', filename: 'Codefile', mime: 'text/code' }); guess = guessMimeTypes(URI.file('Codefile')); - assert.deepEqual(guess, ['text/code', 'text/plain']); + assert.deepStrictEqual(guess, ['text/code', 'text/plain']); guess = guessMimeTypes(URI.file('foo.Codefile')); - assert.deepEqual(guess, ['application/unknown']); + assert.deepStrictEqual(guess, ['application/unknown']); registerTextMime({ id: 'docker', filepattern: 'Docker*', mime: 'text/docker' }); guess = guessMimeTypes(URI.file('Docker-debug')); - assert.deepEqual(guess, ['text/docker', 'text/plain']); + assert.deepStrictEqual(guess, ['text/docker', 'text/plain']); guess = guessMimeTypes(URI.file('docker-PROD')); - assert.deepEqual(guess, ['text/docker', 'text/plain']); + assert.deepStrictEqual(guess, ['text/docker', 'text/plain']); registerTextMime({ id: 'niceregex', mime: 'text/nice-regex', firstline: /RegexesAreNice/ }); guess = guessMimeTypes(URI.file('Randomfile.noregistration'), 'RegexesAreNice'); - assert.deepEqual(guess, ['text/nice-regex', 'text/plain']); + assert.deepStrictEqual(guess, ['text/nice-regex', 'text/plain']); guess = guessMimeTypes(URI.file('Randomfile.noregistration'), 'RegexesAreNotNice'); - assert.deepEqual(guess, ['application/unknown']); + assert.deepStrictEqual(guess, ['application/unknown']); guess = guessMimeTypes(URI.file('Codefile'), 'RegexesAreNice'); - assert.deepEqual(guess, ['text/code', 'text/plain']); + assert.deepStrictEqual(guess, ['text/code', 'text/plain']); }); test('Mimes Priority', () => { @@ -50,36 +50,36 @@ suite('Mime', () => { registerTextMime({ id: 'foobar', mime: 'text/foobar', firstline: /foobar/ }); let guess = guessMimeTypes(URI.file('foo.monaco')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); guess = guessMimeTypes(URI.file('foo.monaco'), 'foobar'); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); registerTextMime({ id: 'docker', filename: 'dockerfile', mime: 'text/winner' }); registerTextMime({ id: 'docker', filepattern: 'dockerfile*', mime: 'text/looser' }); guess = guessMimeTypes(URI.file('dockerfile')); - assert.deepEqual(guess, ['text/winner', 'text/plain']); + assert.deepStrictEqual(guess, ['text/winner', 'text/plain']); registerTextMime({ id: 'azure-looser', mime: 'text/azure-looser', firstline: /azure/ }); registerTextMime({ id: 'azure-winner', mime: 'text/azure-winner', firstline: /azure/ }); guess = guessMimeTypes(URI.file('azure'), 'azure'); - assert.deepEqual(guess, ['text/azure-winner', 'text/plain']); + assert.deepStrictEqual(guess, ['text/azure-winner', 'text/plain']); }); test('Specificity priority 1', () => { registerTextMime({ id: 'monaco2', extension: '.monaco2', mime: 'text/monaco2' }); registerTextMime({ id: 'monaco2', filename: 'specific.monaco2', mime: 'text/specific-monaco2' }); - assert.deepEqual(guessMimeTypes(URI.file('specific.monaco2')), ['text/specific-monaco2', 'text/plain']); - assert.deepEqual(guessMimeTypes(URI.file('foo.monaco2')), ['text/monaco2', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.file('specific.monaco2')), ['text/specific-monaco2', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.file('foo.monaco2')), ['text/monaco2', 'text/plain']); }); test('Specificity priority 2', () => { registerTextMime({ id: 'monaco3', filename: 'specific.monaco3', mime: 'text/specific-monaco3' }); registerTextMime({ id: 'monaco3', extension: '.monaco3', mime: 'text/monaco3' }); - assert.deepEqual(guessMimeTypes(URI.file('specific.monaco3')), ['text/specific-monaco3', 'text/plain']); - assert.deepEqual(guessMimeTypes(URI.file('foo.monaco3')), ['text/monaco3', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.file('specific.monaco3')), ['text/specific-monaco3', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.file('foo.monaco3')), ['text/monaco3', 'text/plain']); }); test('Mimes Priority - Longest Extension wins', () => { @@ -88,13 +88,13 @@ suite('Mime', () => { registerTextMime({ id: 'monaco', extension: '.monaco.xml.build', mime: 'text/monaco-xml-build' }); let guess = guessMimeTypes(URI.file('foo.monaco')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); guess = guessMimeTypes(URI.file('foo.monaco.xml')); - assert.deepEqual(guess, ['text/monaco-xml', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco-xml', 'text/plain']); guess = guessMimeTypes(URI.file('foo.monaco.xml.build')); - assert.deepEqual(guess, ['text/monaco-xml-build', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco-xml-build', 'text/plain']); }); test('Mimes Priority - User configured wins', () => { @@ -102,7 +102,7 @@ suite('Mime', () => { registerTextMime({ id: 'monaco', extension: '.monaco.xml', mime: 'text/monaco-xml' }); let guess = guessMimeTypes(URI.file('foo.monaco.xnl')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); }); test('Mimes Priority - Pattern matches on path if specified', () => { @@ -110,7 +110,7 @@ suite('Mime', () => { registerTextMime({ id: 'other', filepattern: '*ot.other.xml', mime: 'text/other' }); let guess = guessMimeTypes(URI.file('/some/path/dot.monaco.xml')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); }); test('Mimes Priority - Last registered mime wins', () => { @@ -118,12 +118,12 @@ suite('Mime', () => { registerTextMime({ id: 'other', filepattern: '**/dot.monaco.xml', mime: 'text/other' }); let guess = guessMimeTypes(URI.file('/some/path/dot.monaco.xml')); - assert.deepEqual(guess, ['text/other', 'text/plain']); + assert.deepStrictEqual(guess, ['text/other', 'text/plain']); }); test('Data URIs', () => { registerTextMime({ id: 'data', extension: '.data', mime: 'text/data' }); - assert.deepEqual(guessMimeTypes(URI.parse(`data:;label:something.data;description:data,`)), ['text/data', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.parse(`data:;label:something.data;description:data,`)), ['text/data', 'text/plain']); }); }); diff --git a/src/vs/base/test/common/network.test.ts b/src/vs/base/test/common/network.test.ts index 1729d4b1c..240c1abc2 100644 --- a/src/vs/base/test/common/network.test.ts +++ b/src/vs/base/test/common/network.test.ts @@ -7,10 +7,10 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; -import { isElectronSandboxed } from 'vs/base/common/platform'; +import { isPreferringBrowserCodeLoad } from 'vs/base/common/platform'; suite('network', () => { - const enableTest = isElectronSandboxed; + const enableTest = isPreferringBrowserCodeLoad; (!enableTest ? test.skip : test)('FileAccess: URI (native)', () => { diff --git a/src/vs/base/test/common/paging.test.ts b/src/vs/base/test/common/paging.test.ts index 3ce696633..da7ef18fa 100644 --- a/src/vs/base/test/common/paging.test.ts +++ b/src/vs/base/test/common/paging.test.ts @@ -101,7 +101,6 @@ suite('PagedModel', () => { test('preemptive cancellation works', async function () { const pager = new TestPager(() => { assert(false); - return Promise.resolve([]); }); const model = new PagedModel(pager); diff --git a/src/vs/base/test/node/path.test.ts b/src/vs/base/test/common/path.test.ts similarity index 99% rename from src/vs/base/test/node/path.test.ts rename to src/vs/base/test/common/path.test.ts index 3150f8d60..d74ce9f7c 100644 --- a/src/vs/base/test/node/path.test.ts +++ b/src/vs/base/test/common/path.test.ts @@ -29,10 +29,11 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; -import { isWindows } from 'vs/base/common/platform'; +import { isWeb, isWindows } from 'vs/base/common/platform'; import * as process from 'vs/base/common/process'; suite('Paths (Node Implementation)', () => { + const __filename = 'path.test.js'; test('join', () => { const failures = [] as string[]; const backslashRE = /\\/g; @@ -175,9 +176,6 @@ suite('Paths (Node Implementation)', () => { }); test('dirname', () => { - assert.strictEqual(path.dirname(path.normalize(__filename)).substr(-9), - isWindows ? 'test\\node' : 'test/node'); - assert.strictEqual(path.posix.dirname('/a/b/'), '/a'); assert.strictEqual(path.posix.dirname('/a/b'), '/a'); assert.strictEqual(path.posix.dirname('/a'), '/'); @@ -362,7 +360,7 @@ suite('Paths (Node Implementation)', () => { assert.equal(path.extname('far.boo/boo'), ''); }); - test('resolve', () => { + (isWeb && isWindows ? test.skip : test)('resolve', () => { // TODO@sbatten fails on windows & browser only const failures = [] as string[]; const slashRE = /\//g; const backslashRE = /\\/g; diff --git a/src/vs/base/test/common/processes.test.ts b/src/vs/base/test/common/processes.test.ts index ee0d25c88..b5a89e2a0 100644 --- a/src/vs/base/test/common/processes.test.ts +++ b/src/vs/base/test/common/processes.test.ts @@ -15,7 +15,6 @@ suite('Processes', () => { ELECTRON_NO_ASAR: 'x', ELECTRON_NO_ATTACH_CONSOLE: 'x', ELECTRON_RUN_AS_NODE: 'x', - GOOGLE_API_KEY: 'x', VSCODE_CLI: 'x', VSCODE_DEV: 'x', VSCODE_IPC_HOOK: 'x', diff --git a/src/vs/base/test/common/resources.test.ts b/src/vs/base/test/common/resources.test.ts index 8026e240b..a41725d83 100644 --- a/src/vs/base/test/common/resources.test.ts +++ b/src/vs/base/test/common/resources.test.ts @@ -22,10 +22,10 @@ suite('Resources', () => { ]; let distinct = distinctParents(resources, r => r); - assert.equal(distinct.length, 3); - assert.equal(distinct[0].toString(), resources[0].toString()); - assert.equal(distinct[1].toString(), resources[1].toString()); - assert.equal(distinct[2].toString(), resources[2].toString()); + assert.strictEqual(distinct.length, 3); + assert.strictEqual(distinct[0].toString(), resources[0].toString()); + assert.strictEqual(distinct[1].toString(), resources[1].toString()); + assert.strictEqual(distinct[2].toString(), resources[2].toString()); // Parent / Child resources = [ @@ -37,144 +37,144 @@ suite('Resources', () => { ]; distinct = distinctParents(resources, r => r); - assert.equal(distinct.length, 3); - assert.equal(distinct[0].toString(), resources[0].toString()); - assert.equal(distinct[1].toString(), resources[3].toString()); - assert.equal(distinct[2].toString(), resources[4].toString()); + assert.strictEqual(distinct.length, 3); + assert.strictEqual(distinct[0].toString(), resources[0].toString()); + assert.strictEqual(distinct[1].toString(), resources[3].toString()); + assert.strictEqual(distinct[2].toString(), resources[4].toString()); }); test('dirname', () => { if (isWindows) { - assert.equal(dirname(URI.file('c:\\some\\file\\test.txt')).toString(), 'file:///c%3A/some/file'); - assert.equal(dirname(URI.file('c:\\some\\file')).toString(), 'file:///c%3A/some'); - assert.equal(dirname(URI.file('c:\\some\\file\\')).toString(), 'file:///c%3A/some'); - assert.equal(dirname(URI.file('c:\\some')).toString(), 'file:///c%3A/'); - assert.equal(dirname(URI.file('C:\\some')).toString(), 'file:///c%3A/'); - assert.equal(dirname(URI.file('c:\\')).toString(), 'file:///c%3A/'); + assert.strictEqual(dirname(URI.file('c:\\some\\file\\test.txt')).toString(), 'file:///c%3A/some/file'); + assert.strictEqual(dirname(URI.file('c:\\some\\file')).toString(), 'file:///c%3A/some'); + assert.strictEqual(dirname(URI.file('c:\\some\\file\\')).toString(), 'file:///c%3A/some'); + assert.strictEqual(dirname(URI.file('c:\\some')).toString(), 'file:///c%3A/'); + assert.strictEqual(dirname(URI.file('C:\\some')).toString(), 'file:///c%3A/'); + assert.strictEqual(dirname(URI.file('c:\\')).toString(), 'file:///c%3A/'); } else { - assert.equal(dirname(URI.file('/some/file/test.txt')).toString(), 'file:///some/file'); - assert.equal(dirname(URI.file('/some/file/')).toString(), 'file:///some'); - assert.equal(dirname(URI.file('/some/file')).toString(), 'file:///some'); + assert.strictEqual(dirname(URI.file('/some/file/test.txt')).toString(), 'file:///some/file'); + assert.strictEqual(dirname(URI.file('/some/file/')).toString(), 'file:///some'); + assert.strictEqual(dirname(URI.file('/some/file')).toString(), 'file:///some'); } - assert.equal(dirname(URI.parse('foo://a/some/file/test.txt')).toString(), 'foo://a/some/file'); - assert.equal(dirname(URI.parse('foo://a/some/file/')).toString(), 'foo://a/some'); - assert.equal(dirname(URI.parse('foo://a/some/file')).toString(), 'foo://a/some'); - assert.equal(dirname(URI.parse('foo://a/some')).toString(), 'foo://a/'); - assert.equal(dirname(URI.parse('foo://a/')).toString(), 'foo://a/'); - assert.equal(dirname(URI.parse('foo://a')).toString(), 'foo://a'); + assert.strictEqual(dirname(URI.parse('foo://a/some/file/test.txt')).toString(), 'foo://a/some/file'); + assert.strictEqual(dirname(URI.parse('foo://a/some/file/')).toString(), 'foo://a/some'); + assert.strictEqual(dirname(URI.parse('foo://a/some/file')).toString(), 'foo://a/some'); + assert.strictEqual(dirname(URI.parse('foo://a/some')).toString(), 'foo://a/'); + assert.strictEqual(dirname(URI.parse('foo://a/')).toString(), 'foo://a/'); + assert.strictEqual(dirname(URI.parse('foo://a')).toString(), 'foo://a'); // does not explode (https://github.com/microsoft/vscode/issues/41987) dirname(URI.from({ scheme: 'file', authority: '/users/someone/portal.h' })); - assert.equal(dirname(URI.parse('foo://a/b/c?q')).toString(), 'foo://a/b?q'); + assert.strictEqual(dirname(URI.parse('foo://a/b/c?q')).toString(), 'foo://a/b?q'); }); test('basename', () => { if (isWindows) { - assert.equal(basename(URI.file('c:\\some\\file\\test.txt')), 'test.txt'); - assert.equal(basename(URI.file('c:\\some\\file')), 'file'); - assert.equal(basename(URI.file('c:\\some\\file\\')), 'file'); - assert.equal(basename(URI.file('C:\\some\\file\\')), 'file'); + assert.strictEqual(basename(URI.file('c:\\some\\file\\test.txt')), 'test.txt'); + assert.strictEqual(basename(URI.file('c:\\some\\file')), 'file'); + assert.strictEqual(basename(URI.file('c:\\some\\file\\')), 'file'); + assert.strictEqual(basename(URI.file('C:\\some\\file\\')), 'file'); } else { - assert.equal(basename(URI.file('/some/file/test.txt')), 'test.txt'); - assert.equal(basename(URI.file('/some/file/')), 'file'); - assert.equal(basename(URI.file('/some/file')), 'file'); - assert.equal(basename(URI.file('/some')), 'some'); + assert.strictEqual(basename(URI.file('/some/file/test.txt')), 'test.txt'); + assert.strictEqual(basename(URI.file('/some/file/')), 'file'); + assert.strictEqual(basename(URI.file('/some/file')), 'file'); + assert.strictEqual(basename(URI.file('/some')), 'some'); } - assert.equal(basename(URI.parse('foo://a/some/file/test.txt')), 'test.txt'); - assert.equal(basename(URI.parse('foo://a/some/file/')), 'file'); - assert.equal(basename(URI.parse('foo://a/some/file')), 'file'); - assert.equal(basename(URI.parse('foo://a/some')), 'some'); - assert.equal(basename(URI.parse('foo://a/')), ''); - assert.equal(basename(URI.parse('foo://a')), ''); + assert.strictEqual(basename(URI.parse('foo://a/some/file/test.txt')), 'test.txt'); + assert.strictEqual(basename(URI.parse('foo://a/some/file/')), 'file'); + assert.strictEqual(basename(URI.parse('foo://a/some/file')), 'file'); + assert.strictEqual(basename(URI.parse('foo://a/some')), 'some'); + assert.strictEqual(basename(URI.parse('foo://a/')), ''); + assert.strictEqual(basename(URI.parse('foo://a')), ''); }); test('joinPath', () => { if (isWindows) { - assert.equal(joinPath(URI.file('c:\\foo\\bar'), '/file.js').toString(), 'file:///c%3A/foo/bar/file.js'); - assert.equal(joinPath(URI.file('c:\\foo\\bar\\'), 'file.js').toString(), 'file:///c%3A/foo/bar/file.js'); - assert.equal(joinPath(URI.file('c:\\foo\\bar\\'), '/file.js').toString(), 'file:///c%3A/foo/bar/file.js'); - assert.equal(joinPath(URI.file('c:\\'), '/file.js').toString(), 'file:///c%3A/file.js'); - assert.equal(joinPath(URI.file('c:\\'), 'bar/file.js').toString(), 'file:///c%3A/bar/file.js'); - assert.equal(joinPath(URI.file('c:\\foo'), './file.js').toString(), 'file:///c%3A/foo/file.js'); - assert.equal(joinPath(URI.file('c:\\foo'), '/./file.js').toString(), 'file:///c%3A/foo/file.js'); - assert.equal(joinPath(URI.file('C:\\foo'), '../file.js').toString(), 'file:///c%3A/file.js'); - assert.equal(joinPath(URI.file('C:\\foo\\.'), '../file.js').toString(), 'file:///c%3A/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo\\bar'), '/file.js').toString(), 'file:///c%3A/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo\\bar\\'), 'file.js').toString(), 'file:///c%3A/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo\\bar\\'), '/file.js').toString(), 'file:///c%3A/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\'), '/file.js').toString(), 'file:///c%3A/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\'), 'bar/file.js').toString(), 'file:///c%3A/bar/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo'), './file.js').toString(), 'file:///c%3A/foo/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo'), '/./file.js').toString(), 'file:///c%3A/foo/file.js'); + assert.strictEqual(joinPath(URI.file('C:\\foo'), '../file.js').toString(), 'file:///c%3A/file.js'); + assert.strictEqual(joinPath(URI.file('C:\\foo\\.'), '../file.js').toString(), 'file:///c%3A/file.js'); } else { - assert.equal(joinPath(URI.file('/foo/bar'), '/file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/foo/bar'), 'file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/foo/bar/'), '/file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/'), '/file.js').toString(), 'file:///file.js'); - assert.equal(joinPath(URI.file('/foo/bar'), './file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/foo/bar'), '/./file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/foo/bar'), '../file.js').toString(), 'file:///foo/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), '/file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), 'file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar/'), '/file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/'), '/file.js').toString(), 'file:///file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), './file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), '/./file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), '../file.js').toString(), 'file:///foo/file.js'); } - assert.equal(joinPath(URI.parse('foo://a/foo/bar')).toString(), 'foo://a/foo/bar'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar'), '/file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar'), 'file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar/'), '/file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/'), '/file.js').toString(), 'foo://a/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar/'), './file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar/'), '/./file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar/'), '../file.js').toString(), 'foo://a/foo/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar')).toString(), 'foo://a/foo/bar'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar'), '/file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar'), 'file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar/'), '/file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/'), '/file.js').toString(), 'foo://a/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar/'), './file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar/'), '/./file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar/'), '../file.js').toString(), 'foo://a/foo/file.js'); - assert.equal( + assert.strictEqual( joinPath(URI.from({ scheme: 'myScheme', authority: 'authority', path: '/path', query: 'query', fragment: 'fragment' }), '/file.js').toString(), 'myScheme://authority/path/file.js?query#fragment'); }); test('normalizePath', () => { if (isWindows) { - assert.equal(normalizePath(URI.file('c:\\foo\\.\\bar')).toString(), 'file:///c%3A/foo/bar'); - assert.equal(normalizePath(URI.file('c:\\foo\\.')).toString(), 'file:///c%3A/foo'); - assert.equal(normalizePath(URI.file('c:\\foo\\.\\')).toString(), 'file:///c%3A/foo/'); - assert.equal(normalizePath(URI.file('c:\\foo\\..')).toString(), 'file:///c%3A/'); - assert.equal(normalizePath(URI.file('c:\\foo\\..\\bar')).toString(), 'file:///c%3A/bar'); - assert.equal(normalizePath(URI.file('c:\\foo\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); - assert.equal(normalizePath(URI.file('c:\\foo\\foo\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); - assert.equal(normalizePath(URI.file('C:\\foo\\foo\\.\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); - assert.equal(normalizePath(URI.file('C:\\foo\\foo\\.\\..\\some\\..\\bar')).toString(), 'file:///c%3A/foo/bar'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\.\\bar')).toString(), 'file:///c%3A/foo/bar'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\.')).toString(), 'file:///c%3A/foo'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\.\\')).toString(), 'file:///c%3A/foo/'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\..')).toString(), 'file:///c%3A/'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\..\\bar')).toString(), 'file:///c%3A/bar'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\foo\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); + assert.strictEqual(normalizePath(URI.file('C:\\foo\\foo\\.\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); + assert.strictEqual(normalizePath(URI.file('C:\\foo\\foo\\.\\..\\some\\..\\bar')).toString(), 'file:///c%3A/foo/bar'); } else { - assert.equal(normalizePath(URI.file('/foo/./bar')).toString(), 'file:///foo/bar'); - assert.equal(normalizePath(URI.file('/foo/.')).toString(), 'file:///foo'); - assert.equal(normalizePath(URI.file('/foo/./')).toString(), 'file:///foo/'); - assert.equal(normalizePath(URI.file('/foo/..')).toString(), 'file:///'); - assert.equal(normalizePath(URI.file('/foo/../bar')).toString(), 'file:///bar'); - assert.equal(normalizePath(URI.file('/foo/../../bar')).toString(), 'file:///bar'); - assert.equal(normalizePath(URI.file('/foo/foo/../../bar')).toString(), 'file:///bar'); - assert.equal(normalizePath(URI.file('/foo/foo/./../../bar')).toString(), 'file:///bar'); - assert.equal(normalizePath(URI.file('/foo/foo/./../some/../bar')).toString(), 'file:///foo/bar'); - assert.equal(normalizePath(URI.file('/f')).toString(), 'file:///f'); + assert.strictEqual(normalizePath(URI.file('/foo/./bar')).toString(), 'file:///foo/bar'); + assert.strictEqual(normalizePath(URI.file('/foo/.')).toString(), 'file:///foo'); + assert.strictEqual(normalizePath(URI.file('/foo/./')).toString(), 'file:///foo/'); + assert.strictEqual(normalizePath(URI.file('/foo/..')).toString(), 'file:///'); + assert.strictEqual(normalizePath(URI.file('/foo/../bar')).toString(), 'file:///bar'); + assert.strictEqual(normalizePath(URI.file('/foo/../../bar')).toString(), 'file:///bar'); + assert.strictEqual(normalizePath(URI.file('/foo/foo/../../bar')).toString(), 'file:///bar'); + assert.strictEqual(normalizePath(URI.file('/foo/foo/./../../bar')).toString(), 'file:///bar'); + assert.strictEqual(normalizePath(URI.file('/foo/foo/./../some/../bar')).toString(), 'file:///foo/bar'); + assert.strictEqual(normalizePath(URI.file('/f')).toString(), 'file:///f'); } - assert.equal(normalizePath(URI.parse('foo://a/foo/./bar')).toString(), 'foo://a/foo/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/.')).toString(), 'foo://a/foo'); - assert.equal(normalizePath(URI.parse('foo://a/foo/./')).toString(), 'foo://a/foo/'); - assert.equal(normalizePath(URI.parse('foo://a/foo/..')).toString(), 'foo://a/'); - assert.equal(normalizePath(URI.parse('foo://a/foo/../bar')).toString(), 'foo://a/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/../../bar')).toString(), 'foo://a/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/foo/../../bar')).toString(), 'foo://a/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/foo/./../../bar')).toString(), 'foo://a/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/foo/./../some/../bar')).toString(), 'foo://a/foo/bar'); - assert.equal(normalizePath(URI.parse('foo://a')).toString(), 'foo://a'); - assert.equal(normalizePath(URI.parse('foo://a/')).toString(), 'foo://a/'); - assert.equal(normalizePath(URI.parse('foo://a/foo/./bar?q=1')).toString(), URI.parse('foo://a/foo/bar?q%3D1').toString()); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/./bar')).toString(), 'foo://a/foo/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/.')).toString(), 'foo://a/foo'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/./')).toString(), 'foo://a/foo/'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/..')).toString(), 'foo://a/'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/../bar')).toString(), 'foo://a/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/../../bar')).toString(), 'foo://a/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/foo/../../bar')).toString(), 'foo://a/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/foo/./../../bar')).toString(), 'foo://a/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/foo/./../some/../bar')).toString(), 'foo://a/foo/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a')).toString(), 'foo://a'); + assert.strictEqual(normalizePath(URI.parse('foo://a/')).toString(), 'foo://a/'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/./bar?q=1')).toString(), URI.parse('foo://a/foo/bar?q%3D1').toString()); }); test('isAbsolute', () => { if (isWindows) { - assert.equal(isAbsolutePath(URI.file('c:\\foo\\')), true); - assert.equal(isAbsolutePath(URI.file('C:\\foo\\')), true); - assert.equal(isAbsolutePath(URI.file('bar')), true); // URI normalizes all file URIs to be absolute + assert.strictEqual(isAbsolutePath(URI.file('c:\\foo\\')), true); + assert.strictEqual(isAbsolutePath(URI.file('C:\\foo\\')), true); + assert.strictEqual(isAbsolutePath(URI.file('bar')), true); // URI normalizes all file URIs to be absolute } else { - assert.equal(isAbsolutePath(URI.file('/foo/bar')), true); - assert.equal(isAbsolutePath(URI.file('bar')), true); // URI normalizes all file URIs to be absolute + assert.strictEqual(isAbsolutePath(URI.file('/foo/bar')), true); + assert.strictEqual(isAbsolutePath(URI.file('bar')), true); // URI normalizes all file URIs to be absolute } - assert.equal(isAbsolutePath(URI.parse('foo:foo')), false); - assert.equal(isAbsolutePath(URI.parse('foo://a/foo/.')), true); + assert.strictEqual(isAbsolutePath(URI.parse('foo:foo')), false); + assert.strictEqual(isAbsolutePath(URI.parse('foo://a/foo/.')), true); }); function assertTrailingSeparator(u1: URI, expected: boolean) { - assert.equal(hasTrailingPathSeparator(u1), expected, u1.toString()); + assert.strictEqual(hasTrailingPathSeparator(u1), expected, u1.toString()); } function assertRemoveTrailingSeparator(u1: URI, expected: URI) { @@ -237,14 +237,14 @@ suite('Resources', () => { function assertEqualURI(actual: URI, expected: URI, message?: string, ignoreCase?: boolean) { let util = ignoreCase ? extUriIgnorePathCase : extUri; if (!util.isEqual(expected, actual)) { - assert.equal(actual.toString(), expected.toString(), message); + assert.strictEqual(actual.toString(), expected.toString(), message); } } function assertRelativePath(u1: URI, u2: URI, expectedPath: string | undefined, ignoreJoin?: boolean, ignoreCase?: boolean) { let util = ignoreCase ? extUriIgnorePathCase : extUri; - assert.equal(util.relativePath(u1, u2), expectedPath, `from ${u1.toString()} to ${u2.toString()}`); + assert.strictEqual(util.relativePath(u1, u2), expectedPath, `from ${u1.toString()} to ${u2.toString()}`); if (expectedPath !== undefined && !ignoreJoin) { assertEqualURI(removeTrailingPathSeparator(joinPath(u1, expectedPath)), removeTrailingPathSeparator(u2), 'joinPath on relativePath should be equal', ignoreCase); } @@ -304,7 +304,7 @@ suite('Resources', () => { if (!p.isAbsolute(path)) { let expectedPath = isWindows ? toSlashes(path) : path; expectedPath = expectedPath.startsWith('./') ? expectedPath.substr(2) : expectedPath; - assert.equal(relativePath(u1, actual), expectedPath, `relativePath (${u1.toString()}) on actual (${actual.toString()}) should be to path (${expectedPath})`); + assert.strictEqual(relativePath(u1, actual), expectedPath, `relativePath (${u1.toString()}) on actual (${actual.toString()}) should be to path (${expectedPath})`); } } @@ -352,12 +352,12 @@ suite('Resources', () => { let util = ignoreCase ? extUriIgnorePathCase : extUri; - assert.equal(util.isEqual(u1, u2), expected, `${u1.toString()}${expected ? '===' : '!=='}${u2.toString()}`); - assert.equal(util.compare(u1, u2) === 0, expected); - assert.equal(util.getComparisonKey(u1) === util.getComparisonKey(u2), expected, `comparison keys ${u1.toString()}, ${u2.toString()}`); - assert.equal(util.isEqualOrParent(u1, u2), expected, `isEqualOrParent ${u1.toString()}, ${u2.toString()}`); + assert.strictEqual(util.isEqual(u1, u2), expected, `${u1.toString()}${expected ? '===' : '!=='}${u2.toString()}`); + assert.strictEqual(util.compare(u1, u2) === 0, expected); + assert.strictEqual(util.getComparisonKey(u1) === util.getComparisonKey(u2), expected, `comparison keys ${u1.toString()}, ${u2.toString()}`); + assert.strictEqual(util.isEqualOrParent(u1, u2), expected, `isEqualOrParent ${u1.toString()}, ${u2.toString()}`); if (!ignoreCase) { - assert.equal(u1.toString() === u2.toString(), expected); + assert.strictEqual(u1.toString() === u2.toString(), expected); } } @@ -402,31 +402,31 @@ suite('Resources', () => { let fileURI = isWindows ? URI.file('c:\\foo\\bar') : URI.file('/foo/bar'); let fileURI2 = isWindows ? URI.file('c:\\foo') : URI.file('/foo'); let fileURI2b = isWindows ? URI.file('C:\\Foo\\') : URI.file('/Foo/'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI), true, '1'); - assert.equal(extUri.isEqualOrParent(fileURI, fileURI), true, '2'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2), true, '3'); - assert.equal(extUri.isEqualOrParent(fileURI, fileURI2), true, '4'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2b), true, '5'); - assert.equal(extUri.isEqualOrParent(fileURI, fileURI2b), false, '6'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI), true, '1'); + assert.strictEqual(extUri.isEqualOrParent(fileURI, fileURI), true, '2'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2), true, '3'); + assert.strictEqual(extUri.isEqualOrParent(fileURI, fileURI2), true, '4'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2b), true, '5'); + assert.strictEqual(extUri.isEqualOrParent(fileURI, fileURI2b), false, '6'); - assert.equal(extUri.isEqualOrParent(fileURI2, fileURI), false, '7'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI2b, fileURI2), true, '8'); + assert.strictEqual(extUri.isEqualOrParent(fileURI2, fileURI), false, '7'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI2b, fileURI2), true, '8'); let fileURI3 = URI.parse('foo://server:453/foo/bar/goo'); let fileURI4 = URI.parse('foo://server:453/foo/'); let fileURI5 = URI.parse('foo://server:453/foo'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI3, true), true, '11'); - assert.equal(extUri.isEqualOrParent(fileURI3, fileURI3), true, '12'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI4, true), true, '13'); - assert.equal(extUri.isEqualOrParent(fileURI3, fileURI4), true, '14'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI, true), false, '15'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI5, fileURI5, true), true, '16'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI3, true), true, '11'); + assert.strictEqual(extUri.isEqualOrParent(fileURI3, fileURI3), true, '12'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI4, true), true, '13'); + assert.strictEqual(extUri.isEqualOrParent(fileURI3, fileURI4), true, '14'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI, true), false, '15'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI5, fileURI5, true), true, '16'); let fileURI6 = URI.parse('foo://server:453/foo?q=1'); let fileURI7 = URI.parse('foo://server:453/foo/bar?q=1'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI5), false, '17'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI6), true, '18'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI6), true, '19'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI5), false, '20'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI5), false, '17'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI6), true, '18'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI6), true, '19'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI5), false, '20'); }); }); diff --git a/src/vs/base/test/common/scrollable.test.ts b/src/vs/base/test/common/scrollable.test.ts index 820862f2f..bf8e6cb00 100644 --- a/src/vs/base/test/common/scrollable.test.ts +++ b/src/vs/base/test/common/scrollable.test.ts @@ -57,7 +57,7 @@ suite('SmoothScrollingOperation', () => { function assertSmoothScroll(from: number, to: number, expected: [number, number][]): void { const actual = simulateSmoothScroll(from, to); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('scroll 25 lines (40 fit)', () => { diff --git a/src/vs/base/test/common/skipList.test.ts b/src/vs/base/test/common/skipList.test.ts index f6a083e3c..ec3da0d10 100644 --- a/src/vs/base/test/common/skipList.test.ts +++ b/src/vs/base/test/common/skipList.test.ts @@ -12,45 +12,45 @@ import { binarySearch } from 'vs/base/common/arrays'; suite('SkipList', function () { function assertValues(list: SkipList, expected: V[]) { - assert.equal(list.size, expected.length); - assert.deepEqual([...list.values()], expected); + assert.strictEqual(list.size, expected.length); + assert.deepStrictEqual([...list.values()], expected); let valuesFromEntries = [...list.entries()].map(entry => entry[1]); - assert.deepEqual(valuesFromEntries, expected); + assert.deepStrictEqual(valuesFromEntries, expected); let valuesFromIter = [...list].map(entry => entry[1]); - assert.deepEqual(valuesFromIter, expected); + assert.deepStrictEqual(valuesFromIter, expected); let i = 0; list.forEach((value, _key, map) => { assert.ok(map === list); - assert.deepEqual(value, expected[i++]); + assert.deepStrictEqual(value, expected[i++]); }); } function assertKeys(list: SkipList, expected: K[]) { - assert.equal(list.size, expected.length); - assert.deepEqual([...list.keys()], expected); + assert.strictEqual(list.size, expected.length); + assert.deepStrictEqual([...list.keys()], expected); let keysFromEntries = [...list.entries()].map(entry => entry[0]); - assert.deepEqual(keysFromEntries, expected); + assert.deepStrictEqual(keysFromEntries, expected); let keysFromIter = [...list].map(entry => entry[0]); - assert.deepEqual(keysFromIter, expected); + assert.deepStrictEqual(keysFromIter, expected); let i = 0; list.forEach((_value, key, map) => { assert.ok(map === list); - assert.deepEqual(key, expected[i++]); + assert.deepStrictEqual(key, expected[i++]); }); } test('set/get/delete', function () { let list = new SkipList((a, b) => a - b); - assert.equal(list.get(3), undefined); + assert.strictEqual(list.get(3), undefined); list.set(3, 1); - assert.equal(list.get(3), 1); + assert.strictEqual(list.get(3), 1); assertValues(list, [1]); list.set(3, 3); @@ -58,17 +58,17 @@ suite('SkipList', function () { list.set(1, 1); list.set(4, 4); - assert.equal(list.get(3), 3); - assert.equal(list.get(1), 1); - assert.equal(list.get(4), 4); + assert.strictEqual(list.get(3), 3); + assert.strictEqual(list.get(1), 1); + assert.strictEqual(list.get(4), 4); assertValues(list, [1, 3, 4]); - assert.equal(list.delete(17), false); + assert.strictEqual(list.delete(17), false); - assert.equal(list.delete(1), true); - assert.equal(list.get(1), undefined); - assert.equal(list.get(3), 3); - assert.equal(list.get(4), 4); + assert.strictEqual(list.delete(1), true); + assert.strictEqual(list.get(1), undefined); + assert.strictEqual(list.get(3), 3); + assert.strictEqual(list.get(4), 4); assertValues(list, [3, 4]); }); @@ -87,7 +87,7 @@ suite('SkipList', function () { assertKeys(list, [3, 6, 7, 9, 12, 19, 21, 25]); list.set(17, true); - assert.deepEqual(list.size, 9); + assert.deepStrictEqual(list.size, 9); assertKeys(list, [3, 6, 7, 9, 12, 17, 19, 21, 25]); }); @@ -141,8 +141,7 @@ suite('SkipList', function () { } - test('perf', function () { - this.skip(); + test.skip('perf', function () { // data const max = 2 ** 16; diff --git a/src/vs/base/test/common/stream.test.ts b/src/vs/base/test/common/stream.test.ts index 0ef3b7730..0fb36467b 100644 --- a/src/vs/base/test/common/stream.test.ts +++ b/src/vs/base/test/common/stream.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { isReadableStream, newWriteableStream, Readable, consumeReadable, peekReadable, consumeStream, ReadableStream, toStream, toReadable, transform, peekStream, isReadableBufferedStream } from 'vs/base/common/stream'; +import { isReadableStream, newWriteableStream, Readable, consumeReadable, peekReadable, consumeStream, ReadableStream, toStream, toReadable, transform, peekStream, isReadableBufferedStream, observe } from 'vs/base/common/stream'; import { timeout } from 'vs/base/common/async'; suite('Stream', () => { @@ -43,37 +43,37 @@ suite('Stream', () => { chunks.push(data); }); - assert.equal(chunks[0], 'Hello'); + assert.strictEqual(chunks[0], 'Hello'); stream.write('World'); - assert.equal(chunks[1], 'World'); + assert.strictEqual(chunks[1], 'World'); - assert.equal(error, false); - assert.equal(end, false); + assert.strictEqual(error, false); + assert.strictEqual(end, false); stream.pause(); stream.write('1'); stream.write('2'); stream.write('3'); - assert.equal(chunks.length, 2); + assert.strictEqual(chunks.length, 2); stream.resume(); - assert.equal(chunks.length, 3); - assert.equal(chunks[2], '1,2,3'); + assert.strictEqual(chunks.length, 3); + assert.strictEqual(chunks[2], '1,2,3'); stream.error(new Error()); - assert.equal(error, true); + assert.strictEqual(error, true); stream.end('Final Bit'); - assert.equal(chunks.length, 4); - assert.equal(chunks[3], 'Final Bit'); + assert.strictEqual(chunks.length, 4); + assert.strictEqual(chunks[3], 'Final Bit'); stream.destroy(); stream.write('Unexpected'); - assert.equal(chunks.length, 4); + assert.strictEqual(chunks.length, 4); }); test('WriteableStream - removeListener', () => { @@ -92,22 +92,24 @@ suite('Stream', () => { stream.on('data', dataListener); stream.write('Hello'); - assert.equal(data, true); + assert.strictEqual(data, true); data = false; stream.removeListener('data', dataListener); stream.write('World'); - assert.equal(data, false); + assert.strictEqual(data, false); stream.error(new Error()); - assert.equal(error, true); + assert.strictEqual(error, true); error = false; stream.removeListener('error', errorListener); + // always leave at least one error listener to streams to avoid unexpected errors during test running + stream.on('error', () => { }); stream.error(new Error()); - assert.equal(error, false); + assert.strictEqual(error, false); }); test('WriteableStream - highWaterMark', async () => { @@ -147,14 +149,14 @@ suite('Stream', () => { assert.ok(data); await timeout(0); - assert.equal(drained1, true); - assert.equal(drained2, true); + assert.strictEqual(drained1, true); + assert.strictEqual(drained2, true); }); test('consumeReadable', () => { const readable = arrayToReadable(['1', '2', '3', '4', '5']); const consumed = consumeReadable(readable, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); }); test('peekReadable', () => { @@ -166,17 +168,17 @@ suite('Stream', () => { assert.fail('Unexpected result'); } else { const consumed = consumeReadable(consumedOrReadable, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); } } let readable = arrayToReadable(['1', '2', '3', '4', '5']); let consumedOrReadable = peekReadable(readable, strings => strings.join(), 5); - assert.equal(consumedOrReadable, '1,2,3,4,5'); + assert.strictEqual(consumedOrReadable, '1,2,3,4,5'); readable = arrayToReadable(['1', '2', '3', '4', '5']); consumedOrReadable = peekReadable(readable, strings => strings.join(), 6); - assert.equal(consumedOrReadable, '1,2,3,4,5'); + assert.strictEqual(consumedOrReadable, '1,2,3,4,5'); }); test('peekReadable - error handling', async () => { @@ -265,7 +267,7 @@ suite('Stream', () => { test('consumeStream', async () => { const stream = readableToStream(arrayToReadable(['1', '2', '3', '4', '5'])); const consumed = await consumeStream(stream, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); }); test('peekStream', async () => { @@ -273,11 +275,11 @@ suite('Stream', () => { const stream = readableToStream(arrayToReadable(['1', '2', '3', '4', '5'])); const result = await peekStream(stream, i); - assert.equal(stream, result.stream); + assert.strictEqual(stream, result.stream); if (result.ended) { assert.fail('Unexpected result, stream should not have ended yet'); } else { - assert.equal(result.buffer.length, i + 1, `maxChunks: ${i}`); + assert.strictEqual(result.buffer.length, i + 1, `maxChunks: ${i}`); const additionalResult: string[] = []; await consumeStream(stream, strings => { @@ -286,33 +288,33 @@ suite('Stream', () => { return strings.join(); }); - assert.equal([...result.buffer, ...additionalResult].join(), '1,2,3,4,5'); + assert.strictEqual([...result.buffer, ...additionalResult].join(), '1,2,3,4,5'); } } let stream = readableToStream(arrayToReadable(['1', '2', '3', '4', '5'])); let result = await peekStream(stream, 5); - assert.equal(stream, result.stream); - assert.equal(result.buffer.join(), '1,2,3,4,5'); - assert.equal(result.ended, true); + assert.strictEqual(stream, result.stream); + assert.strictEqual(result.buffer.join(), '1,2,3,4,5'); + assert.strictEqual(result.ended, true); stream = readableToStream(arrayToReadable(['1', '2', '3', '4', '5'])); result = await peekStream(stream, 6); - assert.equal(stream, result.stream); - assert.equal(result.buffer.join(), '1,2,3,4,5'); - assert.equal(result.ended, true); + assert.strictEqual(stream, result.stream); + assert.strictEqual(result.buffer.join(), '1,2,3,4,5'); + assert.strictEqual(result.ended, true); }); test('toStream', async () => { const stream = toStream('1,2,3,4,5', strings => strings.join()); const consumed = await consumeStream(stream, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); }); test('toReadable', async () => { const readable = toReadable('1,2,3,4,5'); const consumed = await consumeReadable(readable, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); }); test('transform', async () => { @@ -330,6 +332,47 @@ suite('Stream', () => { }, 0); const consumed = await consumeStream(result, strings => strings.join()); - assert.equal(consumed, '11,22,33,44,55'); + assert.strictEqual(consumed, '11,22,33,44,55'); + }); + + test('observer', async () => { + const source1 = newWriteableStream(strings => strings.join()); + setTimeout(() => source1.error(new Error())); + await observe(source1).errorOrEnd(); + + const source2 = newWriteableStream(strings => strings.join()); + setTimeout(() => source2.end('Hello Test')); + await observe(source2).errorOrEnd(); + + const source3 = newWriteableStream(strings => strings.join()); + setTimeout(() => { + source3.write('Hello Test'); + source3.error(new Error()); + }); + await observe(source3).errorOrEnd(); + + const source4 = newWriteableStream(strings => strings.join()); + setTimeout(() => { + source4.write('Hello Test'); + source4.end(); + }); + await observe(source4).errorOrEnd(); + }); + + test('events are delivered even if a listener is removed during delivery', () => { + const stream = newWriteableStream(strings => strings.join()); + + let listener1Called = false; + let listener2Called = false; + + const listener1 = () => { stream.removeListener('end', listener1); listener1Called = true; }; + const listener2 = () => { listener2Called = true; }; + stream.on('end', listener1); + stream.on('end', listener2); + stream.on('data', () => { }); + stream.end(''); + + assert.strictEqual(listener1Called, true); + assert.strictEqual(listener2Called, true); }); }); diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index d3366a1e5..dc8d9da5e 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -52,7 +52,7 @@ suite('Strings', () => { let expected = strings.compare(a.toLowerCase(), b.toLowerCase()); expected = expected > 0 ? 1 : expected < 0 ? -1 : expected; - assert.equal(actual, expected, `${a} <> ${b}`); + assert.strictEqual(actual, expected, `${a} <> ${b}`); if (recurse) { assertCompareIgnoreCase(b, a, false); @@ -89,7 +89,7 @@ suite('Strings', () => { let expected = strings.compare(a.toLowerCase().substring(aStart, aEnd), b.toLowerCase().substring(bStart, bEnd)); expected = expected > 0 ? 1 : expected < 0 ? -1 : expected; - assert.equal(actual, expected, `${a} <> ${b}`); + assert.strictEqual(actual, expected, `${a} <> ${b}`); if (recurse) { assertCompareIgnoreCase(b, a, bStart, bEnd, aStart, aEnd, false); @@ -188,36 +188,36 @@ suite('Strings', () => { }); test('containsRTL', () => { - assert.equal(strings.containsRTL('a'), false); - assert.equal(strings.containsRTL(''), false); - assert.equal(strings.containsRTL(strings.UTF8_BOM_CHARACTER + 'a'), false); - assert.equal(strings.containsRTL('hello world!'), false); - assert.equal(strings.containsRTL('a📚📚b'), false); - assert.equal(strings.containsRTL('هناك حقيقة مثبتة منذ زمن طويل'), true); - assert.equal(strings.containsRTL('זוהי עובדה מבוססת שדעתו'), true); + assert.strictEqual(strings.containsRTL('a'), false); + assert.strictEqual(strings.containsRTL(''), false); + assert.strictEqual(strings.containsRTL(strings.UTF8_BOM_CHARACTER + 'a'), false); + assert.strictEqual(strings.containsRTL('hello world!'), false); + assert.strictEqual(strings.containsRTL('a📚📚b'), false); + assert.strictEqual(strings.containsRTL('هناك حقيقة مثبتة منذ زمن طويل'), true); + assert.strictEqual(strings.containsRTL('זוהי עובדה מבוססת שדעתו'), true); }); test('containsEmoji', () => { - assert.equal(strings.containsEmoji('a'), false); - assert.equal(strings.containsEmoji(''), false); - assert.equal(strings.containsEmoji(strings.UTF8_BOM_CHARACTER + 'a'), false); - assert.equal(strings.containsEmoji('hello world!'), false); - assert.equal(strings.containsEmoji('هناك حقيقة مثبتة منذ زمن طويل'), false); - assert.equal(strings.containsEmoji('זוהי עובדה מבוססת שדעתו'), false); + assert.strictEqual(strings.containsEmoji('a'), false); + assert.strictEqual(strings.containsEmoji(''), false); + assert.strictEqual(strings.containsEmoji(strings.UTF8_BOM_CHARACTER + 'a'), false); + assert.strictEqual(strings.containsEmoji('hello world!'), false); + assert.strictEqual(strings.containsEmoji('هناك حقيقة مثبتة منذ زمن طويل'), false); + assert.strictEqual(strings.containsEmoji('זוהי עובדה מבוססת שדעתו'), false); - assert.equal(strings.containsEmoji('a📚📚b'), true); - assert.equal(strings.containsEmoji('1F600 # 😀 grinning face'), true); - assert.equal(strings.containsEmoji('1F47E # 👾 alien monster'), true); - assert.equal(strings.containsEmoji('1F467 1F3FD # 👧🏽 girl: medium skin tone'), true); - assert.equal(strings.containsEmoji('26EA # ⛪ church'), true); - assert.equal(strings.containsEmoji('231B # ⌛ hourglass'), true); - assert.equal(strings.containsEmoji('2702 # ✂ scissors'), true); - assert.equal(strings.containsEmoji('1F1F7 1F1F4 # 🇷🇴 Romania'), true); + assert.strictEqual(strings.containsEmoji('a📚📚b'), true); + assert.strictEqual(strings.containsEmoji('1F600 # 😀 grinning face'), true); + assert.strictEqual(strings.containsEmoji('1F47E # 👾 alien monster'), true); + assert.strictEqual(strings.containsEmoji('1F467 1F3FD # 👧🏽 girl: medium skin tone'), true); + assert.strictEqual(strings.containsEmoji('26EA # ⛪ church'), true); + assert.strictEqual(strings.containsEmoji('231B # ⌛ hourglass'), true); + assert.strictEqual(strings.containsEmoji('2702 # ✂ scissors'), true); + assert.strictEqual(strings.containsEmoji('1F1F7 1F1F4 # 🇷🇴 Romania'), true); }); test('isBasicASCII', () => { function assertIsBasicASCII(str: string, expected: boolean): void { - assert.equal(strings.isBasicASCII(str), expected, str + ` (${str.charCodeAt(0)})`); + assert.strictEqual(strings.isBasicASCII(str), expected, str + ` (${str.charCodeAt(0)})`); } assertIsBasicASCII('abcdefghijklmnopqrstuvwxyz', true); assertIsBasicASCII('ABCDEFGHIJKLMNOPQRSTUVWXYZ', true); @@ -245,16 +245,16 @@ suite('Strings', () => { assert.throws(() => strings.createRegExp('', false)); // Escapes appropriately - assert.equal(strings.createRegExp('abc', false).source, 'abc'); - assert.equal(strings.createRegExp('([^ ,.]*)', false).source, '\\(\\[\\^ ,\\.\\]\\*\\)'); - assert.equal(strings.createRegExp('([^ ,.]*)', true).source, '([^ ,.]*)'); + assert.strictEqual(strings.createRegExp('abc', false).source, 'abc'); + assert.strictEqual(strings.createRegExp('([^ ,.]*)', false).source, '\\(\\[\\^ ,\\.\\]\\*\\)'); + assert.strictEqual(strings.createRegExp('([^ ,.]*)', true).source, '([^ ,.]*)'); // Whole word - assert.equal(strings.createRegExp('abc', false, { wholeWord: true }).source, '\\babc\\b'); - assert.equal(strings.createRegExp('abc', true, { wholeWord: true }).source, '\\babc\\b'); - assert.equal(strings.createRegExp(' abc', true, { wholeWord: true }).source, ' abc\\b'); - assert.equal(strings.createRegExp('abc ', true, { wholeWord: true }).source, '\\babc '); - assert.equal(strings.createRegExp(' abc ', true, { wholeWord: true }).source, ' abc '); + assert.strictEqual(strings.createRegExp('abc', false, { wholeWord: true }).source, '\\babc\\b'); + assert.strictEqual(strings.createRegExp('abc', true, { wholeWord: true }).source, '\\babc\\b'); + assert.strictEqual(strings.createRegExp(' abc', true, { wholeWord: true }).source, ' abc\\b'); + assert.strictEqual(strings.createRegExp('abc ', true, { wholeWord: true }).source, '\\babc '); + assert.strictEqual(strings.createRegExp(' abc ', true, { wholeWord: true }).source, ' abc '); const regExpWithoutFlags = strings.createRegExp('abc', true); assert(!regExpWithoutFlags.global); @@ -284,15 +284,15 @@ suite('Strings', () => { }); test('getLeadingWhitespace', () => { - assert.equal(strings.getLeadingWhitespace(' foo'), ' '); - assert.equal(strings.getLeadingWhitespace(' foo', 2), ''); - assert.equal(strings.getLeadingWhitespace(' foo', 1, 1), ''); - assert.equal(strings.getLeadingWhitespace(' foo', 0, 1), ' '); - assert.equal(strings.getLeadingWhitespace(' '), ' '); - assert.equal(strings.getLeadingWhitespace(' ', 1), ' '); - assert.equal(strings.getLeadingWhitespace(' ', 0, 1), ' '); - assert.equal(strings.getLeadingWhitespace('\t\tfunction foo(){', 0, 1), '\t'); - assert.equal(strings.getLeadingWhitespace('\t\tfunction foo(){', 0, 2), '\t\t'); + assert.strictEqual(strings.getLeadingWhitespace(' foo'), ' '); + assert.strictEqual(strings.getLeadingWhitespace(' foo', 2), ''); + assert.strictEqual(strings.getLeadingWhitespace(' foo', 1, 1), ''); + assert.strictEqual(strings.getLeadingWhitespace(' foo', 0, 1), ' '); + assert.strictEqual(strings.getLeadingWhitespace(' '), ' '); + assert.strictEqual(strings.getLeadingWhitespace(' ', 1), ' '); + assert.strictEqual(strings.getLeadingWhitespace(' ', 0, 1), ' '); + assert.strictEqual(strings.getLeadingWhitespace('\t\tfunction foo(){', 0, 1), '\t'); + assert.strictEqual(strings.getLeadingWhitespace('\t\tfunction foo(){', 0, 2), '\t\t'); }); test('fuzzyContains', () => { @@ -316,11 +316,11 @@ suite('Strings', () => { }); test('stripUTF8BOM', () => { - assert.equal(strings.stripUTF8BOM(strings.UTF8_BOM_CHARACTER), ''); - assert.equal(strings.stripUTF8BOM(strings.UTF8_BOM_CHARACTER + 'foobar'), 'foobar'); - assert.equal(strings.stripUTF8BOM('foobar' + strings.UTF8_BOM_CHARACTER), 'foobar' + strings.UTF8_BOM_CHARACTER); - assert.equal(strings.stripUTF8BOM('abc'), 'abc'); - assert.equal(strings.stripUTF8BOM(''), ''); + assert.strictEqual(strings.stripUTF8BOM(strings.UTF8_BOM_CHARACTER), ''); + assert.strictEqual(strings.stripUTF8BOM(strings.UTF8_BOM_CHARACTER + 'foobar'), 'foobar'); + assert.strictEqual(strings.stripUTF8BOM('foobar' + strings.UTF8_BOM_CHARACTER), 'foobar' + strings.UTF8_BOM_CHARACTER); + assert.strictEqual(strings.stripUTF8BOM('abc'), 'abc'); + assert.strictEqual(strings.stripUTF8BOM(''), ''); }); test('containsUppercaseCharacter', () => { @@ -340,7 +340,7 @@ suite('Strings', () => { ['FöÖ', true], ['\\Foo', true], ].forEach(([str, result]) => { - assert.equal(strings.containsUppercaseCharacter(str), result, `Wrong result for ${str}`); + assert.strictEqual(strings.containsUppercaseCharacter(str), result, `Wrong result for ${str}`); }); }); @@ -352,7 +352,7 @@ suite('Strings', () => { ['Foo', true], ].forEach(([str, result]) => { - assert.equal(strings.containsUppercaseCharacter(str, true), result, `Wrong result for ${str}`); + assert.strictEqual(strings.containsUppercaseCharacter(str, true), result, `Wrong result for ${str}`); }); }); @@ -364,20 +364,20 @@ suite('Strings', () => { ['123', '123'], ['.a', '.a'], ].forEach(([inStr, result]) => { - assert.equal(strings.uppercaseFirstLetter(inStr), result, `Wrong result for ${inStr}`); + assert.strictEqual(strings.uppercaseFirstLetter(inStr), result, `Wrong result for ${inStr}`); }); }); test('getNLines', () => { - assert.equal(strings.getNLines('', 5), ''); - assert.equal(strings.getNLines('foo', 5), 'foo'); - assert.equal(strings.getNLines('foo\nbar', 5), 'foo\nbar'); - assert.equal(strings.getNLines('foo\nbar', 2), 'foo\nbar'); + assert.strictEqual(strings.getNLines('', 5), ''); + assert.strictEqual(strings.getNLines('foo', 5), 'foo'); + assert.strictEqual(strings.getNLines('foo\nbar', 5), 'foo\nbar'); + assert.strictEqual(strings.getNLines('foo\nbar', 2), 'foo\nbar'); - assert.equal(strings.getNLines('foo\nbar', 1), 'foo'); - assert.equal(strings.getNLines('foo\nbar'), 'foo'); - assert.equal(strings.getNLines('foo\nbar\nsomething', 2), 'foo\nbar'); - assert.equal(strings.getNLines('foo', 0), ''); + assert.strictEqual(strings.getNLines('foo\nbar', 1), 'foo'); + assert.strictEqual(strings.getNLines('foo\nbar'), 'foo'); + assert.strictEqual(strings.getNLines('foo\nbar\nsomething', 2), 'foo\nbar'); + assert.strictEqual(strings.getNLines('foo', 0), ''); }); test('encodeUTF8', function () { @@ -387,12 +387,12 @@ suite('Strings', () => { for (let offset = 0; offset < actual.byteLength; offset++) { actualArr[offset] = actual[offset]; } - assert.deepEqual(actualArr, expected); + assert.deepStrictEqual(actualArr, expected); } function assertDecodeUTF8(data: number[], expected: string): void { const actual = strings.decodeUTF8(new Uint8Array(data)); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } function assertEncodeDecodeUTF8(str: string, buff: number[]): void { @@ -415,11 +415,11 @@ suite('Strings', () => { }); test('getGraphemeBreakType', () => { - assert.equal(strings.getGraphemeBreakType(0xBC1), strings.GraphemeBreakType.SpacingMark); + assert.strictEqual(strings.getGraphemeBreakType(0xBC1), strings.GraphemeBreakType.SpacingMark); }); test('truncate', () => { - assert.equal('hello world', strings.truncate('hello world', 100)); - assert.equal('hello…', strings.truncate('hello world', 5)); + assert.strictEqual('hello world', strings.truncate('hello world', 100)); + assert.strictEqual('hello…', strings.truncate('hello world', 5)); }); }); diff --git a/src/vs/base/test/common/troubleshooting.ts b/src/vs/base/test/common/troubleshooting.ts new file mode 100644 index 000000000..1d9d99632 --- /dev/null +++ b/src/vs/base/test/common/troubleshooting.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, IDisposableTracker, setDisposableTracker } from 'vs/base/common/lifecycle'; + +class DisposableTracker implements IDisposableTracker { + allDisposables: [IDisposable, string][] = []; + trackDisposable(x: IDisposable): void { + this.allDisposables.push([x, new Error().stack!]); + } + markTracked(x: IDisposable): void { + for (let idx = 0; idx < this.allDisposables.length; idx++) { + if (this.allDisposables[idx][0] === x) { + this.allDisposables.splice(idx, 1); + return; + } + } + } +} + +let currentTracker: DisposableTracker | null = null; + +export function beginTrackingDisposables(): void { + currentTracker = new DisposableTracker(); + setDisposableTracker(currentTracker); +} + +export function endTrackingDisposables(): void { + if (currentTracker) { + setDisposableTracker(null); + console.log(currentTracker!.allDisposables.map(e => `${e[0]}\n${e[1]}`).join('\n\n')); + currentTracker = null; + } +} + +export function beginLoggingFS(withStacks: boolean = false): void { + if ((self).beginLoggingFS) { + (self).beginLoggingFS(withStacks); + } +} + +export function endLoggingFS(): void { + if ((self).endLoggingFS) { + (self).endLoggingFS(); + } +} diff --git a/src/vs/base/test/common/types.test.ts b/src/vs/base/test/common/types.test.ts index 0bec27fcd..cb7dedbe2 100644 --- a/src/vs/base/test/common/types.test.ts +++ b/src/vs/base/test/common/types.test.ts @@ -57,7 +57,7 @@ suite('Types', () => { assert(!types.isObject(/test/)); assert(!types.isObject(new RegExp(''))); assert(!types.isFunction(new Date())); - assert(!types.isObject(assert)); + assert.strictEqual(types.isObject(assert), false); assert(!types.isObject(function foo() { })); assert(types.isObject({})); @@ -75,7 +75,7 @@ suite('Types', () => { assert(!types.isEmptyObject(/test/)); assert(!types.isEmptyObject(new RegExp(''))); assert(!types.isEmptyObject(new Date())); - assert(!types.isEmptyObject(assert)); + assert.strictEqual(types.isEmptyObject(assert), false); assert(!types.isEmptyObject(function foo() { /**/ })); assert(!types.isEmptyObject({ foo: 'bar' })); @@ -178,15 +178,15 @@ suite('Types', () => { assert.throws(() => types.assertAllDefined(true, undefined)); assert.throws(() => types.assertAllDefined(undefined, false)); - assert.equal(types.assertIsDefined(true), true); - assert.equal(types.assertIsDefined(false), false); - assert.equal(types.assertIsDefined('Hello'), 'Hello'); - assert.equal(types.assertIsDefined(''), ''); + assert.strictEqual(types.assertIsDefined(true), true); + assert.strictEqual(types.assertIsDefined(false), false); + assert.strictEqual(types.assertIsDefined('Hello'), 'Hello'); + assert.strictEqual(types.assertIsDefined(''), ''); const res = types.assertAllDefined(1, true, 'Hello'); - assert.equal(res[0], 1); - assert.equal(res[1], true); - assert.equal(res[2], 'Hello'); + assert.strictEqual(res[0], 1); + assert.strictEqual(res[1], true); + assert.strictEqual(res[2], 'Hello'); }); test('validateConstraints', () => { diff --git a/src/vs/base/test/common/uri.test.ts b/src/vs/base/test/common/uri.test.ts index bc4e409c0..a05e4fc3d 100644 --- a/src/vs/base/test/common/uri.test.ts +++ b/src/vs/base/test/common/uri.test.ts @@ -9,82 +9,82 @@ import { isWindows } from 'vs/base/common/platform'; suite('URI', () => { test('file#toString', () => { - assert.equal(URI.file('c:/win/path').toString(), 'file:///c%3A/win/path'); - assert.equal(URI.file('C:/win/path').toString(), 'file:///c%3A/win/path'); - assert.equal(URI.file('c:/win/path/').toString(), 'file:///c%3A/win/path/'); - assert.equal(URI.file('/c:/win/path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('c:/win/path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('C:/win/path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('c:/win/path/').toString(), 'file:///c%3A/win/path/'); + assert.strictEqual(URI.file('/c:/win/path').toString(), 'file:///c%3A/win/path'); }); test('URI.file (win-special)', () => { if (isWindows) { - assert.equal(URI.file('c:\\win\\path').toString(), 'file:///c%3A/win/path'); - assert.equal(URI.file('c:\\win/path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('c:\\win\\path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('c:\\win/path').toString(), 'file:///c%3A/win/path'); } else { - assert.equal(URI.file('c:\\win\\path').toString(), 'file:///c%3A%5Cwin%5Cpath'); - assert.equal(URI.file('c:\\win/path').toString(), 'file:///c%3A%5Cwin/path'); + assert.strictEqual(URI.file('c:\\win\\path').toString(), 'file:///c%3A%5Cwin%5Cpath'); + assert.strictEqual(URI.file('c:\\win/path').toString(), 'file:///c%3A%5Cwin/path'); } }); test('file#fsPath (win-special)', () => { if (isWindows) { - assert.equal(URI.file('c:\\win\\path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('c:\\win/path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('c:\\win\\path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('c:\\win/path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('c:/win/path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('c:/win/path/').fsPath, 'c:\\win\\path\\'); - assert.equal(URI.file('C:/win/path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('/c:/win/path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('./c/win/path').fsPath, '\\.\\c\\win\\path'); + assert.strictEqual(URI.file('c:/win/path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('c:/win/path/').fsPath, 'c:\\win\\path\\'); + assert.strictEqual(URI.file('C:/win/path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('/c:/win/path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('./c/win/path').fsPath, '\\.\\c\\win\\path'); } else { - assert.equal(URI.file('c:/win/path').fsPath, 'c:/win/path'); - assert.equal(URI.file('c:/win/path/').fsPath, 'c:/win/path/'); - assert.equal(URI.file('C:/win/path').fsPath, 'c:/win/path'); - assert.equal(URI.file('/c:/win/path').fsPath, 'c:/win/path'); - assert.equal(URI.file('./c/win/path').fsPath, '/./c/win/path'); + assert.strictEqual(URI.file('c:/win/path').fsPath, 'c:/win/path'); + assert.strictEqual(URI.file('c:/win/path/').fsPath, 'c:/win/path/'); + assert.strictEqual(URI.file('C:/win/path').fsPath, 'c:/win/path'); + assert.strictEqual(URI.file('/c:/win/path').fsPath, 'c:/win/path'); + assert.strictEqual(URI.file('./c/win/path').fsPath, '/./c/win/path'); } }); test('URI#fsPath - no `fsPath` when no `path`', () => { const value = URI.parse('file://%2Fhome%2Fticino%2Fdesktop%2Fcpluscplus%2Ftest.cpp'); - assert.equal(value.authority, '/home/ticino/desktop/cpluscplus/test.cpp'); - assert.equal(value.path, '/'); + assert.strictEqual(value.authority, '/home/ticino/desktop/cpluscplus/test.cpp'); + assert.strictEqual(value.path, '/'); if (isWindows) { - assert.equal(value.fsPath, '\\'); + assert.strictEqual(value.fsPath, '\\'); } else { - assert.equal(value.fsPath, '/'); + assert.strictEqual(value.fsPath, '/'); } }); test('http#toString', () => { - assert.equal(URI.from({ scheme: 'http', authority: 'www.msft.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); - assert.equal(URI.from({ scheme: 'http', authority: 'www.msft.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); - assert.equal(URI.from({ scheme: 'http', authority: 'www.MSFT.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); - assert.equal(URI.from({ scheme: 'http', authority: '', path: 'my/path' }).toString(), 'http:/my/path'); - assert.equal(URI.from({ scheme: 'http', authority: '', path: '/my/path' }).toString(), 'http:/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'www.msft.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'www.msft.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'www.MSFT.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: '', path: 'my/path' }).toString(), 'http:/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: '', path: '/my/path' }).toString(), 'http:/my/path'); //http://a-test-site.com/#test=true - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(), 'http://a-test-site.com/?test%3Dtrue'); - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(), 'http://a-test-site.com/#test%3Dtrue'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(), 'http://a-test-site.com/?test%3Dtrue'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(), 'http://a-test-site.com/#test%3Dtrue'); }); test('http#toString, encode=FALSE', () => { - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(true), 'http://a-test-site.com/?test=true'); - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(true), 'http://a-test-site.com/#test=true'); - assert.equal(URI.from({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(true), 'http:/api/files/test.me?t=1234'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(true), 'http://a-test-site.com/?test=true'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(true), 'http://a-test-site.com/#test=true'); + assert.strictEqual(URI.from({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(true), 'http:/api/files/test.me?t=1234'); const value = URI.parse('file://shares/pröjects/c%23/#l12'); - assert.equal(value.authority, 'shares'); - assert.equal(value.path, '/pröjects/c#/'); - assert.equal(value.fragment, 'l12'); - assert.equal(value.toString(), 'file://shares/pr%C3%B6jects/c%23/#l12'); - assert.equal(value.toString(true), 'file://shares/pröjects/c%23/#l12'); + assert.strictEqual(value.authority, 'shares'); + assert.strictEqual(value.path, '/pröjects/c#/'); + assert.strictEqual(value.fragment, 'l12'); + assert.strictEqual(value.toString(), 'file://shares/pr%C3%B6jects/c%23/#l12'); + assert.strictEqual(value.toString(true), 'file://shares/pröjects/c%23/#l12'); const uri2 = URI.parse(value.toString(true)); const uri3 = URI.parse(value.toString()); - assert.equal(uri2.authority, uri3.authority); - assert.equal(uri2.path, uri3.path); - assert.equal(uri2.query, uri3.query); - assert.equal(uri2.fragment, uri3.fragment); + assert.strictEqual(uri2.authority, uri3.authority); + assert.strictEqual(uri2.path, uri3.path); + assert.strictEqual(uri2.query, uri3.query); + assert.strictEqual(uri2.fragment, uri3.fragment); }); test('with, identity', () => { @@ -101,23 +101,23 @@ suite('URI', () => { }); test('with, changes', () => { - assert.equal(URI.parse('before:some/file/path').with({ scheme: 'after' }).toString(), 'after:some/file/path'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(), 'http:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'http', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'http:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'https', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'https:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'HTTP', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTP:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'HTTPS', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTPS:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'boo', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'boo:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.parse('before:some/file/path').with({ scheme: 'after' }).toString(), 'after:some/file/path'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(), 'http:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'http', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'http:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'https', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'https:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'HTTP', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTP:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'HTTPS', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTPS:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'boo', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'boo:/api/files/test.me?t%3D1234'); }); test('with, remove components #8465', () => { - assert.equal(URI.parse('scheme://authority/path').with({ authority: '' }).toString(), 'scheme:/path'); - assert.equal(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ authority: '' }).toString(), 'scheme:/path'); - assert.equal(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ authority: null }).toString(), 'scheme:/path'); - assert.equal(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ path: '' }).toString(), 'scheme://authority'); - assert.equal(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ path: null }).toString(), 'scheme://authority'); - assert.equal(URI.parse('scheme:/path').with({ authority: '' }).toString(), 'scheme:/path'); - assert.equal(URI.parse('scheme:/path').with({ authority: null }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme://authority/path').with({ authority: '' }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ authority: '' }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ authority: null }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ path: '' }).toString(), 'scheme://authority'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ path: null }).toString(), 'scheme://authority'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: '' }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: null }).toString(), 'scheme:/path'); }); test('with, validation', () => { @@ -130,104 +130,104 @@ suite('URI', () => { test('parse', () => { let value = URI.parse('http:/api/files/test.me?t=1234'); - assert.equal(value.scheme, 'http'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/api/files/test.me'); - assert.equal(value.query, 't=1234'); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'http'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/api/files/test.me'); + assert.strictEqual(value.query, 't=1234'); + assert.strictEqual(value.fragment, ''); value = URI.parse('http://api/files/test.me?t=1234'); - assert.equal(value.scheme, 'http'); - assert.equal(value.authority, 'api'); - assert.equal(value.path, '/files/test.me'); - assert.equal(value.query, 't=1234'); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'http'); + assert.strictEqual(value.authority, 'api'); + assert.strictEqual(value.path, '/files/test.me'); + assert.strictEqual(value.query, 't=1234'); + assert.strictEqual(value.fragment, ''); value = URI.parse('file:///c:/test/me'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/c:/test/me'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); - assert.equal(value.fsPath, isWindows ? 'c:\\test\\me' : 'c:/test/me'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/c:/test/me'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fsPath, isWindows ? 'c:\\test\\me' : 'c:/test/me'); value = URI.parse('file://shares/files/c%23/p.cs'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, 'shares'); - assert.equal(value.path, '/files/c#/p.cs'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); - assert.equal(value.fsPath, isWindows ? '\\\\shares\\files\\c#\\p.cs' : '//shares/files/c#/p.cs'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, 'shares'); + assert.strictEqual(value.path, '/files/c#/p.cs'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fsPath, isWindows ? '\\\\shares\\files\\c#\\p.cs' : '//shares/files/c#/p.cs'); value = URI.parse('file:///c:/Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins/c%23/plugin.json'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins/c#/plugin.json'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins/c#/plugin.json'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); value = URI.parse('file:///c:/test %25/path'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/c:/test %/path'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/c:/test %/path'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); value = URI.parse('inmemory:'); - assert.equal(value.scheme, 'inmemory'); - assert.equal(value.authority, ''); - assert.equal(value.path, ''); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'inmemory'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); value = URI.parse('foo:api/files/test'); - assert.equal(value.scheme, 'foo'); - assert.equal(value.authority, ''); - assert.equal(value.path, 'api/files/test'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'foo'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, 'api/files/test'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); value = URI.parse('file:?q'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/'); - assert.equal(value.query, 'q'); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/'); + assert.strictEqual(value.query, 'q'); + assert.strictEqual(value.fragment, ''); value = URI.parse('file:#d'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/'); - assert.equal(value.query, ''); - assert.equal(value.fragment, 'd'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, 'd'); value = URI.parse('f3ile:#d'); - assert.equal(value.scheme, 'f3ile'); - assert.equal(value.authority, ''); - assert.equal(value.path, ''); - assert.equal(value.query, ''); - assert.equal(value.fragment, 'd'); + assert.strictEqual(value.scheme, 'f3ile'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, 'd'); value = URI.parse('foo+bar:path'); - assert.equal(value.scheme, 'foo+bar'); - assert.equal(value.authority, ''); - assert.equal(value.path, 'path'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'foo+bar'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, 'path'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); value = URI.parse('foo-bar:path'); - assert.equal(value.scheme, 'foo-bar'); - assert.equal(value.authority, ''); - assert.equal(value.path, 'path'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'foo-bar'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, 'path'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); value = URI.parse('foo.bar:path'); - assert.equal(value.scheme, 'foo.bar'); - assert.equal(value.authority, ''); - assert.equal(value.path, 'path'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'foo.bar'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, 'path'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); }); test('parse, disallow //path when no authority', () => { @@ -237,132 +237,132 @@ suite('URI', () => { test('URI#file, win-speciale', () => { if (isWindows) { let value = URI.file('c:\\test\\drive'); - assert.equal(value.path, '/c:/test/drive'); - assert.equal(value.toString(), 'file:///c%3A/test/drive'); + assert.strictEqual(value.path, '/c:/test/drive'); + assert.strictEqual(value.toString(), 'file:///c%3A/test/drive'); value = URI.file('\\\\shäres\\path\\c#\\plugin.json'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, 'shäres'); - assert.equal(value.path, '/path/c#/plugin.json'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); - assert.equal(value.toString(), 'file://sh%C3%A4res/path/c%23/plugin.json'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, 'shäres'); + assert.strictEqual(value.path, '/path/c#/plugin.json'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.toString(), 'file://sh%C3%A4res/path/c%23/plugin.json'); value = URI.file('\\\\localhost\\c$\\GitDevelopment\\express'); - assert.equal(value.scheme, 'file'); - assert.equal(value.path, '/c$/GitDevelopment/express'); - assert.equal(value.fsPath, '\\\\localhost\\c$\\GitDevelopment\\express'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); - assert.equal(value.toString(), 'file://localhost/c%24/GitDevelopment/express'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.path, '/c$/GitDevelopment/express'); + assert.strictEqual(value.fsPath, '\\\\localhost\\c$\\GitDevelopment\\express'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.toString(), 'file://localhost/c%24/GitDevelopment/express'); value = URI.file('c:\\test with %\\path'); - assert.equal(value.path, '/c:/test with %/path'); - assert.equal(value.toString(), 'file:///c%3A/test%20with%20%25/path'); + assert.strictEqual(value.path, '/c:/test with %/path'); + assert.strictEqual(value.toString(), 'file:///c%3A/test%20with%20%25/path'); value = URI.file('c:\\test with %25\\path'); - assert.equal(value.path, '/c:/test with %25/path'); - assert.equal(value.toString(), 'file:///c%3A/test%20with%20%2525/path'); + assert.strictEqual(value.path, '/c:/test with %25/path'); + assert.strictEqual(value.toString(), 'file:///c%3A/test%20with%20%2525/path'); value = URI.file('c:\\test with %25\\c#code'); - assert.equal(value.path, '/c:/test with %25/c#code'); - assert.equal(value.toString(), 'file:///c%3A/test%20with%20%2525/c%23code'); + assert.strictEqual(value.path, '/c:/test with %25/c#code'); + assert.strictEqual(value.toString(), 'file:///c%3A/test%20with%20%2525/c%23code'); value = URI.file('\\\\shares'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, 'shares'); - assert.equal(value.path, '/'); // slash is always there + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, 'shares'); + assert.strictEqual(value.path, '/'); // slash is always there value = URI.file('\\\\shares\\'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, 'shares'); - assert.equal(value.path, '/'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, 'shares'); + assert.strictEqual(value.path, '/'); } }); test('VSCode URI module\'s driveLetterPath regex is incorrect, #32961', function () { let uri = URI.parse('file:///_:/path'); - assert.equal(uri.fsPath, isWindows ? '\\_:\\path' : '/_:/path'); + assert.strictEqual(uri.fsPath, isWindows ? '\\_:\\path' : '/_:/path'); }); test('URI#file, no path-is-uri check', () => { // we don't complain here let value = URI.file('file://path/to/file'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/file://path/to/file'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/file://path/to/file'); }); test('URI#file, always slash', () => { let value = URI.file('a.file'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/a.file'); - assert.equal(value.toString(), 'file:///a.file'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/a.file'); + assert.strictEqual(value.toString(), 'file:///a.file'); value = URI.parse(value.toString()); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/a.file'); - assert.equal(value.toString(), 'file:///a.file'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/a.file'); + assert.strictEqual(value.toString(), 'file:///a.file'); }); test('URI.toString, only scheme and query', () => { const value = URI.parse('stuff:?qüery'); - assert.equal(value.toString(), 'stuff:?q%C3%BCery'); + assert.strictEqual(value.toString(), 'stuff:?q%C3%BCery'); }); test('URI#toString, upper-case percent espaces', () => { const value = URI.parse('file://sh%c3%a4res/path'); - assert.equal(value.toString(), 'file://sh%C3%A4res/path'); + assert.strictEqual(value.toString(), 'file://sh%C3%A4res/path'); }); test('URI#toString, lower-case windows drive letter', () => { - assert.equal(URI.parse('untitled:c:/Users/jrieken/Code/abc.txt').toString(), 'untitled:c%3A/Users/jrieken/Code/abc.txt'); - assert.equal(URI.parse('untitled:C:/Users/jrieken/Code/abc.txt').toString(), 'untitled:c%3A/Users/jrieken/Code/abc.txt'); + assert.strictEqual(URI.parse('untitled:c:/Users/jrieken/Code/abc.txt').toString(), 'untitled:c%3A/Users/jrieken/Code/abc.txt'); + assert.strictEqual(URI.parse('untitled:C:/Users/jrieken/Code/abc.txt').toString(), 'untitled:c%3A/Users/jrieken/Code/abc.txt'); }); test('URI#toString, escape all the bits', () => { const value = URI.file('/Users/jrieken/Code/_samples/18500/Mödel + Other Thîngß/model.js'); - assert.equal(value.toString(), 'file:///Users/jrieken/Code/_samples/18500/M%C3%B6del%20%2B%20Other%20Th%C3%AEng%C3%9F/model.js'); + assert.strictEqual(value.toString(), 'file:///Users/jrieken/Code/_samples/18500/M%C3%B6del%20%2B%20Other%20Th%C3%AEng%C3%9F/model.js'); }); test('URI#toString, don\'t encode port', () => { let value = URI.parse('http://localhost:8080/far'); - assert.equal(value.toString(), 'http://localhost:8080/far'); + assert.strictEqual(value.toString(), 'http://localhost:8080/far'); value = URI.from({ scheme: 'http', authority: 'löcalhost:8080', path: '/far', query: undefined, fragment: undefined }); - assert.equal(value.toString(), 'http://l%C3%B6calhost:8080/far'); + assert.strictEqual(value.toString(), 'http://l%C3%B6calhost:8080/far'); }); test('URI#toString, user information in authority', () => { let value = URI.parse('http://foo:bar@localhost/far'); - assert.equal(value.toString(), 'http://foo:bar@localhost/far'); + assert.strictEqual(value.toString(), 'http://foo:bar@localhost/far'); value = URI.parse('http://foo@localhost/far'); - assert.equal(value.toString(), 'http://foo@localhost/far'); + assert.strictEqual(value.toString(), 'http://foo@localhost/far'); value = URI.parse('http://foo:bAr@localhost:8080/far'); - assert.equal(value.toString(), 'http://foo:bAr@localhost:8080/far'); + assert.strictEqual(value.toString(), 'http://foo:bAr@localhost:8080/far'); value = URI.parse('http://foo@localhost:8080/far'); - assert.equal(value.toString(), 'http://foo@localhost:8080/far'); + assert.strictEqual(value.toString(), 'http://foo@localhost:8080/far'); value = URI.from({ scheme: 'http', authority: 'föö:bör@löcalhost:8080', path: '/far', query: undefined, fragment: undefined }); - assert.equal(value.toString(), 'http://f%C3%B6%C3%B6:b%C3%B6r@l%C3%B6calhost:8080/far'); + assert.strictEqual(value.toString(), 'http://f%C3%B6%C3%B6:b%C3%B6r@l%C3%B6calhost:8080/far'); }); test('correctFileUriToFilePath2', () => { const test = (input: string, expected: string) => { const value = URI.parse(input); - assert.equal(value.fsPath, expected, 'Result for ' + input); + assert.strictEqual(value.fsPath, expected, 'Result for ' + input); const value2 = URI.file(value.fsPath); - assert.equal(value2.fsPath, expected, 'Result for ' + input); - assert.equal(value.toString(), value2.toString()); + assert.strictEqual(value2.fsPath, expected, 'Result for ' + input); + assert.strictEqual(value.toString(), value2.toString()); }; test('file:///c:/alex.txt', isWindows ? 'c:\\alex.txt' : 'c:/alex.txt'); @@ -374,104 +374,132 @@ suite('URI', () => { test('URI - http, query & toString', function () { let uri = URI.parse('https://go.microsoft.com/fwlink/?LinkId=518008'); - assert.equal(uri.query, 'LinkId=518008'); - assert.equal(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008'); - assert.equal(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008'); + assert.strictEqual(uri.query, 'LinkId=518008'); + assert.strictEqual(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008'); + assert.strictEqual(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008'); let uri2 = URI.parse(uri.toString()); - assert.equal(uri2.query, 'LinkId=518008'); - assert.equal(uri2.query, uri.query); + assert.strictEqual(uri2.query, 'LinkId=518008'); + assert.strictEqual(uri2.query, uri.query); uri = URI.parse('https://go.microsoft.com/fwlink/?LinkId=518008&foö&ké¥=üü'); - assert.equal(uri.query, 'LinkId=518008&foö&ké¥=üü'); - assert.equal(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008&foö&ké¥=üü'); - assert.equal(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008%26fo%C3%B6%26k%C3%A9%C2%A5%3D%C3%BC%C3%BC'); + assert.strictEqual(uri.query, 'LinkId=518008&foö&ké¥=üü'); + assert.strictEqual(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008&foö&ké¥=üü'); + assert.strictEqual(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008%26fo%C3%B6%26k%C3%A9%C2%A5%3D%C3%BC%C3%BC'); uri2 = URI.parse(uri.toString()); - assert.equal(uri2.query, 'LinkId=518008&foö&ké¥=üü'); - assert.equal(uri2.query, uri.query); + assert.strictEqual(uri2.query, 'LinkId=518008&foö&ké¥=üü'); + assert.strictEqual(uri2.query, uri.query); // #24849 uri = URI.parse('https://twitter.com/search?src=typd&q=%23tag'); - assert.equal(uri.toString(true), 'https://twitter.com/search?src=typd&q=%23tag'); + assert.strictEqual(uri.toString(true), 'https://twitter.com/search?src=typd&q=%23tag'); }); test('class URI cannot represent relative file paths #34449', function () { let path = '/foo/bar'; - assert.equal(URI.file(path).path, path); + assert.strictEqual(URI.file(path).path, path); path = 'foo/bar'; - assert.equal(URI.file(path).path, '/foo/bar'); + assert.strictEqual(URI.file(path).path, '/foo/bar'); path = './foo/bar'; - assert.equal(URI.file(path).path, '/./foo/bar'); // missing normalization + assert.strictEqual(URI.file(path).path, '/./foo/bar'); // missing normalization const fileUri1 = URI.parse(`file:foo/bar`); - assert.equal(fileUri1.path, '/foo/bar'); - assert.equal(fileUri1.authority, ''); + assert.strictEqual(fileUri1.path, '/foo/bar'); + assert.strictEqual(fileUri1.authority, ''); const uri = fileUri1.toString(); - assert.equal(uri, 'file:///foo/bar'); + assert.strictEqual(uri, 'file:///foo/bar'); const fileUri2 = URI.parse(uri); - assert.equal(fileUri2.path, '/foo/bar'); - assert.equal(fileUri2.authority, ''); + assert.strictEqual(fileUri2.path, '/foo/bar'); + assert.strictEqual(fileUri2.authority, ''); }); test('Ctrl click to follow hash query param url gets urlencoded #49628', function () { let input = 'http://localhost:3000/#/foo?bar=baz'; let uri = URI.parse(input); - assert.equal(uri.toString(true), input); + assert.strictEqual(uri.toString(true), input); input = 'http://localhost:3000/foo?bar=baz'; uri = URI.parse(input); - assert.equal(uri.toString(true), input); + assert.strictEqual(uri.toString(true), input); }); test('Unable to open \'%A0.txt\': URI malformed #76506', function () { let uri = URI.file('/foo/%A0.txt'); let uri2 = URI.parse(uri.toString()); - assert.equal(uri.scheme, uri2.scheme); - assert.equal(uri.path, uri2.path); + assert.strictEqual(uri.scheme, uri2.scheme); + assert.strictEqual(uri.path, uri2.path); uri = URI.file('/foo/%2e.txt'); uri2 = URI.parse(uri.toString()); - assert.equal(uri.scheme, uri2.scheme); - assert.equal(uri.path, uri2.path); + assert.strictEqual(uri.scheme, uri2.scheme); + assert.strictEqual(uri.path, uri2.path); + }); + + test('Bug in URI.isUri() that fails `thing` type comparison #114971', function () { + const uri = URI.file('/foo/bazz.txt'); + assert.strictEqual(URI.isUri(uri), true); + assert.strictEqual(URI.isUri(uri.toJSON()), false); + + // fsPath -> getter + assert.strictEqual(URI.isUri({ + scheme: 'file', + authority: '', + path: '/foo/bazz.txt', + get fsPath() { return '/foo/bazz.txt'; }, + query: '', + fragment: '', + with() { return this; }, + toString() { return ''; } + }), true); + + // fsPath -> property + assert.strictEqual(URI.isUri({ + scheme: 'file', + authority: '', + path: '/foo/bazz.txt', + fsPath: '/foo/bazz.txt', + query: '', + fragment: '', + with() { return this; }, + toString() { return ''; } + }), true); }); test('Unable to open \'%A0.txt\': URI malformed #76506', function () { - assert.equal(URI.parse('file://some/%.txt'), 'file://some/%25.txt'); - assert.equal(URI.parse('file://some/%A0.txt'), 'file://some/%25A0.txt'); + assert.strictEqual(URI.parse('file://some/%.txt').toString(), 'file://some/%25.txt'); + assert.strictEqual(URI.parse('file://some/%A0.txt').toString(), 'file://some/%25A0.txt'); }); - test('Links in markdown are broken if url contains encoded parameters #79474', function () { - this.skip(); + test.skip('Links in markdown are broken if url contains encoded parameters #79474', function () { let strIn = 'https://myhost.com/Redirect?url=http%3A%2F%2Fwww.bing.com%3Fsearch%3Dtom'; let uri1 = URI.parse(strIn); let strOut = uri1.toString(); let uri2 = URI.parse(strOut); - assert.equal(uri1.scheme, uri2.scheme); - assert.equal(uri1.authority, uri2.authority); - assert.equal(uri1.path, uri2.path); - assert.equal(uri1.query, uri2.query); - assert.equal(uri1.fragment, uri2.fragment); - assert.equal(strIn, strOut); // fails here!! + assert.strictEqual(uri1.scheme, uri2.scheme); + assert.strictEqual(uri1.authority, uri2.authority); + assert.strictEqual(uri1.path, uri2.path); + assert.strictEqual(uri1.query, uri2.query); + assert.strictEqual(uri1.fragment, uri2.fragment); + assert.strictEqual(strIn, strOut); // fails here!! }); - test('Uri#parse can break path-component #45515', function () { - this.skip(); + test.skip('Uri#parse can break path-component #45515', function () { let strIn = 'https://firebasestorage.googleapis.com/v0/b/brewlangerie.appspot.com/o/products%2FzVNZkudXJyq8bPGTXUxx%2FBetterave-Sesame.jpg?alt=media&token=0b2310c4-3ea6-4207-bbde-9c3710ba0437'; let uri1 = URI.parse(strIn); let strOut = uri1.toString(); let uri2 = URI.parse(strOut); - assert.equal(uri1.scheme, uri2.scheme); - assert.equal(uri1.authority, uri2.authority); - assert.equal(uri1.path, uri2.path); - assert.equal(uri1.query, uri2.query); - assert.equal(uri1.fragment, uri2.fragment); - assert.equal(strIn, strOut); // fails here!! + assert.strictEqual(uri1.scheme, uri2.scheme); + assert.strictEqual(uri1.authority, uri2.authority); + assert.strictEqual(uri1.path, uri2.path); + assert.strictEqual(uri1.query, uri2.query); + assert.strictEqual(uri1.fragment, uri2.fragment); + assert.strictEqual(strIn, strOut); // fails here!! }); test('URI - (de)serialize', function () { @@ -492,13 +520,13 @@ suite('URI', () => { let data = value.toJSON() as UriComponents; let clone = URI.revive(data); - assert.equal(clone.scheme, value.scheme); - assert.equal(clone.authority, value.authority); - assert.equal(clone.path, value.path); - assert.equal(clone.query, value.query); - assert.equal(clone.fragment, value.fragment); - assert.equal(clone.fsPath, value.fsPath); - assert.equal(clone.toString(), value.toString()); + assert.strictEqual(clone.scheme, value.scheme); + assert.strictEqual(clone.authority, value.authority); + assert.strictEqual(clone.path, value.path); + assert.strictEqual(clone.query, value.query); + assert.strictEqual(clone.fragment, value.fragment); + assert.strictEqual(clone.fsPath, value.fsPath); + assert.strictEqual(clone.toString(), value.toString()); } // } // console.profileEnd(); @@ -507,11 +535,11 @@ suite('URI', () => { const baseUri = URI.parse(base); const newUri = URI.joinPath(baseUri, fragment); const actual = newUri.toString(true); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); if (checkWithUrl) { const actualUrl = new URL(fragment, base).href; - assert.equal(actualUrl, expected, 'DIFFERENT from URL'); + assert.strictEqual(actualUrl, expected, 'DIFFERENT from URL'); } } test('URI#joinPath', function () { diff --git a/src/vs/base/test/common/utils.ts b/src/vs/base/test/common/utils.ts index a5ebd13e5..63b0541b4 100644 --- a/src/vs/base/test/common/utils.ts +++ b/src/vs/base/test/common/utils.ts @@ -5,47 +5,10 @@ import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; -import { canceled } from 'vs/base/common/errors'; import { isWindows } from 'vs/base/common/platform'; export type ValueCallback = (value: T | Promise) => void; -export class DeferredPromise { - - private completeCallback!: ValueCallback; - private errorCallback!: (err: any) => void; - - public p: Promise; - - constructor() { - this.p = new Promise((c, e) => { - this.completeCallback = c; - this.errorCallback = e; - }); - } - - public complete(value: T) { - return new Promise(resolve => { - this.completeCallback(value); - resolve(); - }); - } - - public error(err: any) { - return new Promise(resolve => { - this.errorCallback(err); - resolve(); - }); - } - - public cancel() { - new Promise(resolve => { - this.errorCallback(canceled()); - resolve(); - }); - } -} - export function toResource(this: any, path: string) { if (isWindows) { return URI.file(join('C:\\', btoa(this.test.fullTitle()), path)); @@ -60,7 +23,7 @@ export function suiteRepeat(n: number, description: string, callback: (this: any } } -export function testRepeat(n: number, description: string, callback: (this: any, done: MochaDone) => any): void { +export function testRepeat(n: number, description: string, callback: (this: any) => any): void { for (let i = 0; i < n; i++) { test(`${description} (iteration ${i})`, callback); } diff --git a/src/vs/base/test/common/uuid.test.ts b/src/vs/base/test/common/uuid.test.ts index ce07ab9cb..632366bc2 100644 --- a/src/vs/base/test/common/uuid.test.ts +++ b/src/vs/base/test/common/uuid.test.ts @@ -8,8 +8,8 @@ import * as uuid from 'vs/base/common/uuid'; suite('UUID', () => { test('generation', () => { const asHex = uuid.generateUuid(); - assert.equal(asHex.length, 36); - assert.equal(asHex[14], '4'); + assert.strictEqual(asHex.length, 36); + assert.strictEqual(asHex[14], '4'); assert.ok(asHex[19] === '8' || asHex[19] === '9' || asHex[19] === 'a' || asHex[19] === 'b'); }); diff --git a/src/vs/base/test/node/crypto.test.ts b/src/vs/base/test/node/crypto.test.ts index ad8dc4fa5..16cfc58fe 100644 --- a/src/vs/base/test/node/crypto.test.ts +++ b/src/vs/base/test/node/crypto.test.ts @@ -4,24 +4,29 @@ *--------------------------------------------------------------------------------------------*/ import { checksum } from 'vs/base/node/crypto'; -import { generateUuid } from 'vs/base/common/uuid'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { mkdirp, rimraf, RimRafMode, writeFile } from 'vs/base/node/pfs'; +import { mkdirp, rimraf, writeFile } from 'vs/base/node/pfs'; +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; suite('Crypto', () => { + let testDir: string; + + setup(function () { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'crypto'); + + return mkdirp(testDir); + }); + + teardown(function () { + return rimraf(testDir); + }); + test('checksum', async () => { - const id = generateUuid(); - const testDir = join(tmpdir(), 'vsctests', id); const testFile = join(testDir, 'checksum.txt'); - - await mkdirp(testDir); - await writeFile(testFile, 'Hello World'); await checksum(testFile, '0a4d55a8d778e5022fab701977c5d840bbc486d0'); - - await rimraf(testDir, RimRafMode.MOVE); }); }); diff --git a/src/vs/base/test/node/decoder.test.ts b/src/vs/base/test/node/decoder.test.ts index f4d34d02f..aa2e867c7 100644 --- a/src/vs/base/test/node/decoder.test.ts +++ b/src/vs/base/test/node/decoder.test.ts @@ -4,19 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as decoder from 'vs/base/node/decoder'; +import { LineDecoder } from 'vs/base/node/decoder'; suite('Decoder', () => { test('decoding', () => { - const lineDecoder = new decoder.LineDecoder(); + const lineDecoder = new LineDecoder(); let res = lineDecoder.write(Buffer.from('hello')); - assert.equal(res.length, 0); + assert.strictEqual(res.length, 0); res = lineDecoder.write(Buffer.from('\nworld')); - assert.equal(res[0], 'hello'); - assert.equal(res.length, 1); + assert.strictEqual(res[0], 'hello'); + assert.strictEqual(res.length, 1); - assert.equal(lineDecoder.end(), 'world'); + assert.strictEqual(lineDecoder.end(), 'world'); }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/node/extpath.test.ts b/src/vs/base/test/node/extpath.test.ts index e435d6248..056110be8 100644 --- a/src/vs/base/test/node/extpath.test.ts +++ b/src/vs/base/test/node/extpath.test.ts @@ -4,70 +4,52 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; -import * as uuid from 'vs/base/common/uuid'; -import * as pfs from 'vs/base/node/pfs'; +import { tmpdir } from 'os'; +import { mkdirp, rimraf } from 'vs/base/node/pfs'; import { realcaseSync, realpath, realpathSync } from 'vs/base/node/extpath'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; -suite('Extpath', () => { +flakySuite('Extpath', () => { + let testDir: string; + + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'extpath'); + + return mkdirp(testDir, 493); + }); + + teardown(() => { + return rimraf(testDir); + }); test('realcase', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extpath', id); - - await pfs.mkdirp(newDir, 493); // assume case insensitive file system if (process.platform === 'win32' || process.platform === 'darwin') { - const upper = newDir.toUpperCase(); + const upper = testDir.toUpperCase(); const real = realcaseSync(upper); if (real) { // can be null in case of permission errors - assert.notEqual(real, upper); - assert.equal(real.toUpperCase(), upper); - assert.equal(real, newDir); + assert.notStrictEqual(real, upper); + assert.strictEqual(real.toUpperCase(), upper); + assert.strictEqual(real, testDir); } } // linux, unix, etc. -> assume case sensitive file system else { - const real = realcaseSync(newDir); - assert.equal(real, newDir); + const real = realcaseSync(testDir); + assert.strictEqual(real, testDir); } - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); }); test('realpath', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extpath', id); - - await pfs.mkdirp(newDir, 493); - - const realpathVal = await realpath(newDir); + const realpathVal = await realpath(testDir); assert.ok(realpathVal); - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); }); - test('realpathSync', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extpath', id); - - await pfs.mkdirp(newDir, 493); - - let realpath!: string; - try { - realpath = realpathSync(newDir); - } catch (error) { - assert.ok(!error); - } - assert.ok(realpath!); - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + test('realpathSync', () => { + const realpath = realpathSync(testDir); + assert.ok(realpath); }); }); diff --git a/src/vs/base/test/node/id.test.ts b/src/vs/base/test/node/id.test.ts index 637afa5b5..4d1241632 100644 --- a/src/vs/base/test/node/id.test.ts +++ b/src/vs/base/test/node/id.test.ts @@ -2,22 +2,21 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import { getMachineId } from 'vs/base/node/id'; import { getMac } from 'vs/base/node/macAddress'; +import { flakySuite } from 'vs/base/test/node/testUtils'; -suite('ID', () => { +flakySuite('ID', () => { - test('getMachineId', function () { - this.timeout(20000); - return getMachineId().then(id => { - assert.ok(id); - }); + test('getMachineId', async function () { + const id = await getMachineId(); + assert.ok(id); }); - test('getMac', () => { - return getMac().then(macAddress => { - assert.ok(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(macAddress), `Expected a MAC address, got: ${macAddress}`); - }); + test('getMac', async () => { + const macAddress = await getMac(); + assert.ok(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(macAddress), `Expected a MAC address, got: ${macAddress}`); }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/node/keytar.test.ts b/src/vs/base/test/node/keytar.test.ts index b2f7bab50..4e187264b 100644 --- a/src/vs/base/test/node/keytar.test.ts +++ b/src/vs/base/test/node/keytar.test.ts @@ -2,36 +2,30 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; -import * as platform from 'vs/base/common/platform'; +import { isLinux } from 'vs/base/common/platform'; suite('Keytar', () => { - test('loads and is functional', function (done) { - if (platform.isLinux) { - // Skip test due to set up issue with Travis. - this.skip(); - return; - } - (async () => { - const keytar = await import('keytar'); - const name = `VSCode Test ${Math.floor(Math.random() * 1e9)}`; + (isLinux ? test.skip : test)('loads and is functional', async () => { // TODO@RMacfarlane test seems to fail on Linux (Error: Unknown or unsupported transport 'disabled' for address 'disabled:') + const keytar = await import('keytar'); + const name = `VSCode Test ${Math.floor(Math.random() * 1e9)}`; + try { + await keytar.setPassword(name, 'foo', 'bar'); + assert.strictEqual(await keytar.findPassword(name), 'bar'); + assert.strictEqual((await keytar.findCredentials(name)).length, 1); + assert.strictEqual(await keytar.getPassword(name, 'foo'), 'bar'); + await keytar.deletePassword(name, 'foo'); + assert.strictEqual(await keytar.getPassword(name, 'foo'), null); + } catch (err) { + // try to clean up try { - await keytar.setPassword(name, 'foo', 'bar'); - assert.equal(await keytar.findPassword(name), 'bar'); - assert.equal((await keytar.findCredentials(name)).length, 1); - assert.equal(await keytar.getPassword(name, 'foo'), 'bar'); await keytar.deletePassword(name, 'foo'); - assert.equal(await keytar.getPassword(name, 'foo'), undefined); - } catch (err) { - // try to clean up - try { - await keytar.deletePassword(name, 'foo'); - } finally { - // eslint-disable-next-line no-unsafe-finally - throw err; - } + } finally { + // eslint-disable-next-line no-unsafe-finally + throw err; } - })().then(done, done); + } }); }); diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index fd324076b..eeb380a62 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -4,388 +4,306 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; import * as fs from 'fs'; -import * as uuid from 'vs/base/common/uuid'; -import * as pfs from 'vs/base/node/pfs'; +import { tmpdir } from 'os'; +import { join, sep } from 'vs/base/common/path'; +import { generateUuid } from 'vs/base/common/uuid'; +import { copy, exists, mkdirp, move, readdir, readDirsInDir, readdirWithFileTypes, readFile, renameIgnoreError, rimraf, RimRafMode, rimrafSync, statLink, writeFile, writeFileSync } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { getPathFromAmdModule } from 'vs/base/common/amd'; -import { isWindows } from 'vs/base/common/platform'; import { canNormalize } from 'vs/base/common/normalization'; import { VSBuffer } from 'vs/base/common/buffer'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; -suite('PFS', function () { +flakySuite('PFS', function () { - // Given issues such as https://github.com/microsoft/vscode/issues/84066 - // we see random test failures when accessing the native file system. To - // diagnose further, we retry node.js file access tests up to 3 times to - // rule out any random disk issue. - this.retries(3); + let testDir: string; + + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'pfs'); + + return mkdirp(testDir, 493); + }); + + teardown(() => { + return rimraf(testDir); + }); test('writeFile', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'writefile.txt'); + const testFile = join(testDir, 'writefile.txt'); - await pfs.mkdirp(newDir, 493); - assert.ok(fs.existsSync(newDir)); + assert.ok(!(await exists(testFile))); - await pfs.writeFile(testFile, 'Hello World', (null!)); - assert.equal(fs.readFileSync(testFile), 'Hello World'); + await writeFile(testFile, 'Hello World', (null!)); - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + assert.strictEqual((await readFile(testFile)).toString(), 'Hello World'); }); test('writeFile - parallel write on different files works', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile1 = path.join(newDir, 'writefile1.txt'); - const testFile2 = path.join(newDir, 'writefile2.txt'); - const testFile3 = path.join(newDir, 'writefile3.txt'); - const testFile4 = path.join(newDir, 'writefile4.txt'); - const testFile5 = path.join(newDir, 'writefile5.txt'); - - await pfs.mkdirp(newDir, 493); - assert.ok(fs.existsSync(newDir)); + const testFile1 = join(testDir, 'writefile1.txt'); + const testFile2 = join(testDir, 'writefile2.txt'); + const testFile3 = join(testDir, 'writefile3.txt'); + const testFile4 = join(testDir, 'writefile4.txt'); + const testFile5 = join(testDir, 'writefile5.txt'); await Promise.all([ - pfs.writeFile(testFile1, 'Hello World 1', (null!)), - pfs.writeFile(testFile2, 'Hello World 2', (null!)), - pfs.writeFile(testFile3, 'Hello World 3', (null!)), - pfs.writeFile(testFile4, 'Hello World 4', (null!)), - pfs.writeFile(testFile5, 'Hello World 5', (null!)) + writeFile(testFile1, 'Hello World 1', (null!)), + writeFile(testFile2, 'Hello World 2', (null!)), + writeFile(testFile3, 'Hello World 3', (null!)), + writeFile(testFile4, 'Hello World 4', (null!)), + writeFile(testFile5, 'Hello World 5', (null!)) ]); - assert.equal(fs.readFileSync(testFile1), 'Hello World 1'); - assert.equal(fs.readFileSync(testFile2), 'Hello World 2'); - assert.equal(fs.readFileSync(testFile3), 'Hello World 3'); - assert.equal(fs.readFileSync(testFile4), 'Hello World 4'); - assert.equal(fs.readFileSync(testFile5), 'Hello World 5'); - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + assert.strictEqual(fs.readFileSync(testFile1).toString(), 'Hello World 1'); + assert.strictEqual(fs.readFileSync(testFile2).toString(), 'Hello World 2'); + assert.strictEqual(fs.readFileSync(testFile3).toString(), 'Hello World 3'); + assert.strictEqual(fs.readFileSync(testFile4).toString(), 'Hello World 4'); + assert.strictEqual(fs.readFileSync(testFile5).toString(), 'Hello World 5'); }); test('writeFile - parallel write on same files works and is sequentalized', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'writefile.txt'); - - await pfs.mkdirp(newDir, 493); - assert.ok(fs.existsSync(newDir)); + const testFile = join(testDir, 'writefile.txt'); await Promise.all([ - pfs.writeFile(testFile, 'Hello World 1', undefined), - pfs.writeFile(testFile, 'Hello World 2', undefined), - timeout(10).then(() => pfs.writeFile(testFile, 'Hello World 3', undefined)), - pfs.writeFile(testFile, 'Hello World 4', undefined), - timeout(10).then(() => pfs.writeFile(testFile, 'Hello World 5', undefined)) + writeFile(testFile, 'Hello World 1', undefined), + writeFile(testFile, 'Hello World 2', undefined), + timeout(10).then(() => writeFile(testFile, 'Hello World 3', undefined)), + writeFile(testFile, 'Hello World 4', undefined), + timeout(10).then(() => writeFile(testFile, 'Hello World 5', undefined)) ]); - assert.equal(fs.readFileSync(testFile), 'Hello World 5'); - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + assert.strictEqual(fs.readFileSync(testFile).toString(), 'Hello World 5'); }); test('rimraf - simple - unlink', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - await pfs.rimraf(newDir); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - simple - move', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - await pfs.rimraf(newDir, pfs.RimRafMode.MOVE); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir, RimRafMode.MOVE); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - recursive folder structure - unlink', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); + fs.mkdirSync(join(testDir, 'somefolder')); + fs.writeFileSync(join(testDir, 'somefolder', 'somefile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - fs.mkdirSync(path.join(newDir, 'somefolder')); - fs.writeFileSync(path.join(newDir, 'somefolder', 'somefile.txt'), 'Contents'); - - await pfs.rimraf(newDir); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - recursive folder structure - move', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); + fs.mkdirSync(join(testDir, 'somefolder')); + fs.writeFileSync(join(testDir, 'somefolder', 'somefile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - fs.mkdirSync(path.join(newDir, 'somefolder')); - fs.writeFileSync(path.join(newDir, 'somefolder', 'somefile.txt'), 'Contents'); - - await pfs.rimraf(newDir, pfs.RimRafMode.MOVE); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir, RimRafMode.MOVE); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - simple ends with dot - move', async () => { - const id = `${uuid.generateUuid()}.`; - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - await pfs.rimraf(newDir, pfs.RimRafMode.MOVE); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir, RimRafMode.MOVE); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - simple ends with dot slash/backslash - move', async () => { - const id = `${uuid.generateUuid()}.`; - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - await pfs.rimraf(`${newDir}${path.sep}`, pfs.RimRafMode.MOVE); - assert.ok(!fs.existsSync(newDir)); + await rimraf(`${testDir}${sep}`, RimRafMode.MOVE); + assert.ok(!fs.existsSync(testDir)); }); test('rimrafSync - swallows file not found error', function () { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + const nonExistingDir = join(testDir, 'not-existing'); + rimrafSync(nonExistingDir); - pfs.rimrafSync(newDir); - - assert.ok(!fs.existsSync(newDir)); + assert.ok(!fs.existsSync(nonExistingDir)); }); test('rimrafSync - simple', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); + rimrafSync(testDir); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - pfs.rimrafSync(newDir); - - assert.ok(!fs.existsSync(newDir)); + assert.ok(!fs.existsSync(testDir)); }); test('rimrafSync - recursive folder structure', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); + fs.mkdirSync(join(testDir, 'somefolder')); + fs.writeFileSync(join(testDir, 'somefolder', 'somefile.txt'), 'Contents'); - fs.mkdirSync(path.join(newDir, 'somefolder')); - fs.writeFileSync(path.join(newDir, 'somefolder', 'somefile.txt'), 'Contents'); + rimrafSync(testDir); - pfs.rimrafSync(newDir); - - assert.ok(!fs.existsSync(newDir)); + assert.ok(!fs.existsSync(testDir)); }); - test('moveIgnoreError', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - - await pfs.mkdirp(newDir, 493); - try { - await pfs.renameIgnoreError(path.join(newDir, 'foo'), path.join(newDir, 'bar')); - return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); - } - catch (error) { - assert.fail(error); - } + test('moveIgnoreError', () => { + return renameIgnoreError(join(testDir, 'foo'), join(testDir, 'bar')); }); test('copy, move and delete', async () => { - const id = uuid.generateUuid(); - const id2 = uuid.generateUuid(); + const id = generateUuid(); + const id2 = generateUuid(); const sourceDir = getPathFromAmdModule(require, './fixtures'); - const parentDir = path.join(os.tmpdir(), 'vsctests', 'pfs'); - const targetDir = path.join(parentDir, id); - const targetDir2 = path.join(parentDir, id2); + const parentDir = join(tmpdir(), 'vsctests', 'pfs'); + const targetDir = join(parentDir, id); + const targetDir2 = join(parentDir, id2); - await pfs.copy(sourceDir, targetDir); + await copy(sourceDir, targetDir); assert.ok(fs.existsSync(targetDir)); - assert.ok(fs.existsSync(path.join(targetDir, 'index.html'))); - assert.ok(fs.existsSync(path.join(targetDir, 'site.css'))); - assert.ok(fs.existsSync(path.join(targetDir, 'examples'))); - assert.ok(fs.statSync(path.join(targetDir, 'examples')).isDirectory()); - assert.ok(fs.existsSync(path.join(targetDir, 'examples', 'small.jxs'))); + assert.ok(fs.existsSync(join(targetDir, 'index.html'))); + assert.ok(fs.existsSync(join(targetDir, 'site.css'))); + assert.ok(fs.existsSync(join(targetDir, 'examples'))); + assert.ok(fs.statSync(join(targetDir, 'examples')).isDirectory()); + assert.ok(fs.existsSync(join(targetDir, 'examples', 'small.jxs'))); - await pfs.move(targetDir, targetDir2); + await move(targetDir, targetDir2); assert.ok(!fs.existsSync(targetDir)); assert.ok(fs.existsSync(targetDir2)); - assert.ok(fs.existsSync(path.join(targetDir2, 'index.html'))); - assert.ok(fs.existsSync(path.join(targetDir2, 'site.css'))); - assert.ok(fs.existsSync(path.join(targetDir2, 'examples'))); - assert.ok(fs.statSync(path.join(targetDir2, 'examples')).isDirectory()); - assert.ok(fs.existsSync(path.join(targetDir2, 'examples', 'small.jxs'))); + assert.ok(fs.existsSync(join(targetDir2, 'index.html'))); + assert.ok(fs.existsSync(join(targetDir2, 'site.css'))); + assert.ok(fs.existsSync(join(targetDir2, 'examples'))); + assert.ok(fs.statSync(join(targetDir2, 'examples')).isDirectory()); + assert.ok(fs.existsSync(join(targetDir2, 'examples', 'small.jxs'))); - await pfs.move(path.join(targetDir2, 'index.html'), path.join(targetDir2, 'index_moved.html')); + await move(join(targetDir2, 'index.html'), join(targetDir2, 'index_moved.html')); - assert.ok(!fs.existsSync(path.join(targetDir2, 'index.html'))); - assert.ok(fs.existsSync(path.join(targetDir2, 'index_moved.html'))); + assert.ok(!fs.existsSync(join(targetDir2, 'index.html'))); + assert.ok(fs.existsSync(join(targetDir2, 'index_moved.html'))); - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + await rimraf(parentDir); assert.ok(!fs.existsSync(parentDir)); }); - test('mkdirp', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + test('copy skips over dangling symbolic links', async () => { + const id1 = generateUuid(); + const symbolicLinkTarget = join(testDir, id1); - await pfs.mkdirp(newDir, 493); + const id2 = generateUuid(); + const symbolicLink = join(testDir, id2); + + const id3 = generateUuid(); + const copyTarget = join(testDir, id3); + + await mkdirp(symbolicLinkTarget, 493); + + fs.symlinkSync(symbolicLinkTarget, symbolicLink, 'junction'); + + await rimraf(symbolicLinkTarget); + + await copy(symbolicLink, copyTarget); // this should not throw + + assert.ok(!fs.existsSync(copyTarget)); + }); + + test('mkdirp', async () => { + const newDir = join(testDir, generateUuid()); + + await mkdirp(newDir, 493); assert.ok(fs.existsSync(newDir)); - - return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); }); test('readDirsInDir', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.mkdirSync(join(testDir, 'somefolder1')); + fs.mkdirSync(join(testDir, 'somefolder2')); + fs.mkdirSync(join(testDir, 'somefolder3')); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - - fs.mkdirSync(path.join(newDir, 'somefolder1')); - fs.mkdirSync(path.join(newDir, 'somefolder2')); - fs.mkdirSync(path.join(newDir, 'somefolder3')); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - const result = await pfs.readDirsInDir(newDir); - assert.equal(result.length, 3); + const result = await readDirsInDir(testDir); + assert.strictEqual(result.length, 3); assert.ok(result.indexOf('somefolder1') !== -1); assert.ok(result.indexOf('somefolder2') !== -1); assert.ok(result.indexOf('somefolder3') !== -1); - - await pfs.rimraf(newDir); }); test('stat link', async () => { - if (isWindows) { - return; // Symlinks are not the same on win, and we can not create them programitically without admin privileges - } + const id1 = generateUuid(); + const directory = join(testDir, id1); - const id1 = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id1); - const directory = path.join(parentDir, 'pfs', id1); + const id2 = generateUuid(); + const symbolicLink = join(testDir, id2); - const id2 = uuid.generateUuid(); - const symbolicLink = path.join(parentDir, 'pfs', id2); + await mkdirp(directory, 493); - await pfs.mkdirp(directory, 493); + fs.symlinkSync(directory, symbolicLink, 'junction'); - fs.symlinkSync(directory, symbolicLink); - - let statAndIsLink = await pfs.statLink(directory); + let statAndIsLink = await statLink(directory); assert.ok(!statAndIsLink?.symbolicLink); - statAndIsLink = await pfs.statLink(symbolicLink); + statAndIsLink = await statLink(symbolicLink); assert.ok(statAndIsLink?.symbolicLink); assert.ok(!statAndIsLink?.symbolicLink?.dangling); - - pfs.rimrafSync(directory); }); test('stat link (non existing target)', async () => { - if (isWindows) { - return; // Symlinks are not the same on win, and we can not create them programitically without admin privileges - } + const id1 = generateUuid(); + const directory = join(testDir, id1); - const id1 = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id1); - const directory = path.join(parentDir, 'pfs', id1); + const id2 = generateUuid(); + const symbolicLink = join(testDir, id2); - const id2 = uuid.generateUuid(); - const symbolicLink = path.join(parentDir, 'pfs', id2); + await mkdirp(directory, 493); - await pfs.mkdirp(directory, 493); + fs.symlinkSync(directory, symbolicLink, 'junction'); - fs.symlinkSync(directory, symbolicLink); + await rimraf(directory); - pfs.rimrafSync(directory); - - const statAndIsLink = await pfs.statLink(symbolicLink); + const statAndIsLink = await statLink(symbolicLink); assert.ok(statAndIsLink?.symbolicLink); assert.ok(statAndIsLink?.symbolicLink?.dangling); }); test('readdir', async () => { if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id, 'öäü'); + const id = generateUuid(); + const newDir = join(testDir, 'pfs', id, 'öäü'); - await pfs.mkdirp(newDir, 493); + await mkdirp(newDir, 493); assert.ok(fs.existsSync(newDir)); - const children = await pfs.readdir(path.join(parentDir, 'pfs', id)); - assert.equal(children.some(n => n === 'öäü'), true); // Mac always converts to NFD, so - - await pfs.rimraf(parentDir); + const children = await readdir(join(testDir, 'pfs', id)); + assert.strictEqual(children.some(n => n === 'öäü'), true); // Mac always converts to NFD, so } }); test('readdirWithFileTypes', async () => { if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const testDir = path.join(parentDir, 'pfs', id); + const newDir = join(testDir, 'öäü'); + await mkdirp(newDir, 493); - const newDir = path.join(testDir, 'öäü'); - await pfs.mkdirp(newDir, 493); - - await pfs.writeFile(path.join(testDir, 'somefile.txt'), 'contents'); + await writeFile(join(testDir, 'somefile.txt'), 'contents'); assert.ok(fs.existsSync(newDir)); - const children = await pfs.readdirWithFileTypes(testDir); + const children = await readdirWithFileTypes(testDir); - assert.equal(children.some(n => n.name === 'öäü'), true); // Mac always converts to NFD, so - assert.equal(children.some(n => n.isDirectory()), true); + assert.strictEqual(children.some(n => n.name === 'öäü'), true); // Mac always converts to NFD, so + assert.strictEqual(children.some(n => n.isDirectory()), true); - assert.equal(children.some(n => n.name === 'somefile.txt'), true); - assert.equal(children.some(n => n.isFile()), true); - - await pfs.rimraf(parentDir); + assert.strictEqual(children.some(n => n.name === 'somefile.txt'), true); + assert.strictEqual(children.some(n => n.isFile()), true); } }); @@ -416,65 +334,41 @@ suite('PFS', function () { bigData: string | Buffer | Uint8Array, bigDataValue: string ): Promise { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'flushed.txt'); + const testFile = join(testDir, 'flushed.txt'); - await pfs.mkdirp(newDir, 493); - assert.ok(fs.existsSync(newDir)); + assert.ok(fs.existsSync(testDir)); - await pfs.writeFile(testFile, smallData); - assert.equal(fs.readFileSync(testFile), smallDataValue); + await writeFile(testFile, smallData); + assert.strictEqual(fs.readFileSync(testFile).toString(), smallDataValue); - await pfs.writeFile(testFile, bigData); - assert.equal(fs.readFileSync(testFile), bigDataValue); - - await pfs.rimraf(parentDir); + await writeFile(testFile, bigData); + assert.strictEqual(fs.readFileSync(testFile).toString(), bigDataValue); } test('writeFile (string, error handling)', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'flushed.txt'); + const testFile = join(testDir, 'flushed.txt'); - await pfs.mkdirp(newDir, 493); - - assert.ok(fs.existsSync(newDir)); - - fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! + fs.mkdirSync(testFile); // this will trigger an error later because testFile is now a directory! let expectedError: Error | undefined; try { - await pfs.writeFile(testFile, 'Hello World'); + await writeFile(testFile, 'Hello World'); } catch (error) { expectedError = error; } assert.ok(expectedError); - - await pfs.rimraf(parentDir); }); test('writeFileSync', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'flushed.txt'); + const testFile = join(testDir, 'flushed.txt'); - await pfs.mkdirp(newDir, 493); - - assert.ok(fs.existsSync(newDir)); - - pfs.writeFileSync(testFile, 'Hello World'); - assert.equal(fs.readFileSync(testFile), 'Hello World'); + writeFileSync(testFile, 'Hello World'); + assert.strictEqual(fs.readFileSync(testFile).toString(), 'Hello World'); const largeString = (new Array(100 * 1024)).join('Large String\n'); - pfs.writeFileSync(testFile, largeString); - assert.equal(fs.readFileSync(testFile), largeString); - - await pfs.rimraf(parentDir); + writeFileSync(testFile, largeString); + assert.strictEqual(fs.readFileSync(testFile).toString(), largeString); }); }); diff --git a/src/vs/base/test/node/port.test.ts b/src/vs/base/test/node/port.test.ts index 87d8eca9e..120044651 100644 --- a/src/vs/base/test/node/port.test.ts +++ b/src/vs/base/test/node/port.test.ts @@ -6,14 +6,10 @@ import * as assert from 'assert'; import * as net from 'net'; import * as ports from 'vs/base/node/ports'; +import { flakySuite } from 'vs/base/test/node/testUtils'; -suite('Ports', () => { - test('Finds a free port (no timeout)', function (done) { - this.timeout(1000 * 10); // higher timeout for this test - - if (process.env['VSCODE_PID']) { - return done(); // this test fails when run from within VS Code - } +flakySuite('Ports', () => { + (process.env['VSCODE_PID'] ? test.skip /* this test fails when run from within VS Code */ : test)('Finds a free port (no timeout)', function (done) { // get an initial freeport >= 7000 ports.findFreePort(7000, 100, 300000).then(initialPort => { diff --git a/src/vs/base/test/node/powershell.test.ts b/src/vs/base/test/node/powershell.test.ts new file mode 100644 index 000000000..6e88b0982 --- /dev/null +++ b/src/vs/base/test/node/powershell.test.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as platform from 'vs/base/common/platform'; +import { enumeratePowerShellInstallations, getFirstAvailablePowerShellInstallation, IPowerShellExeDetails } from 'vs/base/node/powershell'; + +function checkPath(exePath: string) { + // Check to see if the path exists + let pathCheckResult = false; + try { + const stat = fs.statSync(exePath); + pathCheckResult = stat.isFile(); + } catch { + // fs.exists throws on Windows with SymbolicLinks so we + // also use lstat to try and see if the file exists. + try { + pathCheckResult = fs.statSync(fs.readlinkSync(exePath)).isFile(); + } catch { + + } + } + + assert.strictEqual(pathCheckResult, true); +} + +if (platform.isWindows) { + suite('PowerShell finder', () => { + + test('Can find first available PowerShell', async () => { + const pwshExe = await getFirstAvailablePowerShellInstallation(); + const exePath = pwshExe?.exePath; + assert.notStrictEqual(exePath, null); + assert.notStrictEqual(pwshExe?.displayName, null); + + checkPath(exePath!); + }); + + test('Can enumerate PowerShells', async () => { + const isOS64Bit = os.arch() === 'x64'; + const pwshs = new Array(); + for await (const p of enumeratePowerShellInstallations()) { + pwshs.push(p); + } + + const powershellLog = 'Found these PowerShells:\n' + pwshs.map(p => `${p.displayName}: ${p.exePath}`).join('\n'); + assert.strictEqual(pwshs.length >= (isOS64Bit ? 2 : 1), true, powershellLog); + + for (const pwsh of pwshs) { + checkPath(pwsh.exePath); + } + + + const lastIndex = pwshs.length - 1; + const secondToLastIndex = pwshs.length - 2; + + // 64bit process on 64bit OS + if (process.arch === 'x64') { + checkPath(pwshs[secondToLastIndex].exePath); + assert.strictEqual(pwshs[secondToLastIndex].displayName, 'Windows PowerShell', powershellLog); + + checkPath(pwshs[lastIndex].exePath); + assert.strictEqual(pwshs[lastIndex].displayName, 'Windows PowerShell (x86)', powershellLog); + } else if (isOS64Bit) { + // 32bit process on 64bit OS + + // Windows PowerShell x86 comes first if vscode is 32bit + checkPath(pwshs[secondToLastIndex].exePath); + assert.strictEqual(pwshs[secondToLastIndex].displayName, 'Windows PowerShell (x86)', powershellLog); + + checkPath(pwshs[lastIndex].exePath); + assert.strictEqual(pwshs[lastIndex].displayName, 'Windows PowerShell', powershellLog); + } else { + // 32bit or ARM process + checkPath(pwshs[lastIndex].exePath); + assert.strictEqual(pwshs[lastIndex].displayName, 'Windows PowerShell (x86)', powershellLog); + } + }); + }); +} diff --git a/src/vs/base/test/node/processes/processes.test.ts b/src/vs/base/test/node/processes/processes.test.ts index 76719506d..e9f922f33 100644 --- a/src/vs/base/test/node/processes/processes.test.ts +++ b/src/vs/base/test/node/processes/processes.test.ts @@ -13,9 +13,9 @@ import { getPathFromAmdModule } from 'vs/base/common/amd'; function fork(id: string): cp.ChildProcess { const opts: any = { env: objects.mixin(objects.deepClone(process.env), { - AMD_ENTRYPOINT: id, - PIPE_LOGGING: 'true', - VERBOSE_LOGGING: true + VSCODE_AMD_ENTRYPOINT: id, + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: true }) }; @@ -59,11 +59,7 @@ suite('Processes', () => { }); }); - test('buffered sending - lots of data (potential deadlock on win32)', function (done: () => void) { - if (!platform.isWindows || process.env['VSCODE_PID']) { - return done(); // test is only relevant for Windows and seems to crash randomly on some Linux builds - } - + (!platform.isWindows || process.env['VSCODE_PID'] ? test.skip : test)('buffered sending - lots of data (potential deadlock on win32)', function (done: () => void) { // test is only relevant for Windows and seems to crash randomly on some Linux builds const child = fork('vs/base/test/node/processes/fixtures/fork_large'); const sender = processes.createQueuedSender(child); diff --git a/src/vs/base/test/node/testUtils.ts b/src/vs/base/test/node/testUtils.ts index 452e8ae07..802d9fd14 100644 --- a/src/vs/base/test/node/testUtils.ts +++ b/src/vs/base/test/node/testUtils.ts @@ -3,9 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { Suite } from 'mocha'; import { join } from 'vs/base/common/path'; import { generateUuid } from 'vs/base/common/uuid'; export function getRandomTestPath(tmpdir: string, ...segments: string[]): string { return join(tmpdir, ...segments, generateUuid()); } + +export function flakySuite(title: string, fn: (this: Suite) => void): Suite { + return suite(title, function () { + + // Flaky suites need retries and timeout to complete + // e.g. because they access the file system which can + // be unreliable depending on the environment. + this.retries(3); + this.timeout(1000 * 20); + + // Invoke suite ensuring that `this` is + // properly wired in. + fn.call(this); + }); +} diff --git a/src/vs/base/test/node/zip/zip.test.ts b/src/vs/base/test/node/zip/zip.test.ts index f79bddaa9..a98b2609f 100644 --- a/src/vs/base/test/node/zip/zip.test.ts +++ b/src/vs/base/test/node/zip/zip.test.ts @@ -5,24 +5,33 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; -import * as os from 'os'; +import { tmpdir } from 'os'; import { extract } from 'vs/base/node/zip'; -import { generateUuid } from 'vs/base/common/uuid'; -import { rimraf, exists } from 'vs/base/node/pfs'; +import { rimraf, exists, mkdirp } from 'vs/base/node/pfs'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { createCancelablePromise } from 'vs/base/common/async'; - -const fixtures = getPathFromAmdModule(require, './fixtures'); +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; suite('Zip', () => { - test('extract should handle directories', () => { - const fixture = path.join(fixtures, 'extract.zip'); - const target = path.join(os.tmpdir(), generateUuid()); + let testDir: string; - return createCancelablePromise(token => extract(fixture, target, {}, token) - .then(() => exists(path.join(target, 'extension'))) - .then(exists => assert(exists)) - .then(() => rimraf(target))); + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'zip'); + + return mkdirp(testDir); + }); + + teardown(() => { + return rimraf(testDir); + }); + + test('extract should handle directories', async () => { + const fixtures = getPathFromAmdModule(require, './fixtures'); + const fixture = path.join(fixtures, 'extract.zip'); + + await createCancelablePromise(token => extract(fixture, testDir, {}, token)); + const doesExist = await exists(path.join(testDir, 'extension')); + assert(doesExist); }); }); diff --git a/src/vs/base/worker/defaultWorkerFactory.ts b/src/vs/base/worker/defaultWorkerFactory.ts index 51faabdf9..21b3935fc 100644 --- a/src/vs/base/worker/defaultWorkerFactory.ts +++ b/src/vs/base/worker/defaultWorkerFactory.ts @@ -6,6 +6,8 @@ import { globals } from 'vs/base/common/platform'; import { IWorker, IWorkerCallback, IWorkerFactory, logOnceWebWorkerWarning } from 'vs/base/common/worker/simpleWorker'; +const ttPolicy = window.trustedTypes?.createPolicy('defaultWorkerFactory', { createScriptURL: value => value }); + function getWorker(workerId: string, label: string): Worker | Promise { // Option for hosts to overwrite the worker script (used in the standalone editor) if (globals.MonacoEnvironment) { @@ -13,7 +15,8 @@ function getWorker(workerId: string, label: string): Worker | Promise { return globals.MonacoEnvironment.getWorker(workerId, label); } if (typeof globals.MonacoEnvironment.getWorkerUrl === 'function') { - return new Worker(globals.MonacoEnvironment.getWorkerUrl(workerId, label)); + const wokerUrl = globals.MonacoEnvironment.getWorkerUrl(workerId, label); + return new Worker(ttPolicy ? ttPolicy.createScriptURL(wokerUrl) as unknown as string : wokerUrl, { name: label }); } } // ESM-comment-begin @@ -21,7 +24,7 @@ function getWorker(workerId: string, label: string): Worker | Promise { // check if the JS lives on a different origin const workerMain = require.toUrl('./' + workerId); // explicitly using require.toUrl(), see https://github.com/microsoft/vscode/issues/107440#issuecomment-698982321 const workerUrl = getWorkerBootstrapUrl(workerMain, label); - return new Worker(workerUrl, { name: label }); + return new Worker(ttPolicy ? ttPolicy.createScriptURL(workerUrl) as unknown as string : workerUrl, { name: label }); } // ESM-comment-end throw new Error(`You must define a function MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker`); diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index 8b1869294..2df6f8125 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -3,8 +3,7 @@ @@ -56,7 +55,7 @@ @@ -55,7 +54,7 @@ diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index d1dba0721..1ed7feec9 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -265,7 +265,6 @@ class PollingURLCallbackProvider extends Disposable implements IURLCallbackProvi setTimeout(() => this.periodicFetchCallback(requestId, startTime), PollingURLCallbackProvider.FETCH_INTERVAL); } } - } class WorkspaceProvider implements IWorkspaceProvider { diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner.ts new file mode 100644 index 000000000..0ffa0870e --- /dev/null +++ b/src/vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; + +export class DeprecatedExtensionsCleaner extends Disposable { + + constructor( + @IExtensionManagementService private readonly extensionManagementService: ExtensionManagementService + ) { + super(); + + this._register(extensionManagementService); // TODO@sandy081 this seems fishy + + this.cleanUpDeprecatedExtensions(); + } + + private cleanUpDeprecatedExtensions(): void { + this.extensionManagementService.removeDeprecatedExtensions(); + } +} diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts b/src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts new file mode 100644 index 000000000..6c9bfa812 --- /dev/null +++ b/src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; + +export class LocalizationsUpdater extends Disposable { + + constructor( + @ILocalizationsService private readonly localizationsService: LocalizationsService + ) { + super(); + + this.updateLocalizations(); + } + + private updateLocalizations(): void { + this.localizationsService.update(); + } +} diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js b/src/vs/code/electron-browser/sharedProcess/sharedProcess.js index 8874e8720..2bb0a0bb7 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcess.js @@ -15,10 +15,7 @@ // Load shared process into window bootstrapWindow.load(['vs/code/electron-browser/sharedProcess/sharedProcessMain'], function (sharedProcess, configuration) { - sharedProcess.startup({ - machineId: configuration.machineId, - windowId: configuration.windowId - }); + return sharedProcess.main(configuration); }); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 3dcc360b5..3aa7cce16 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as platform from 'vs/base/common/platform'; import product from 'vs/platform/product/common/product'; -import { serve, Server, connect } from 'vs/base/parts/ipc/node/ipc.net'; +import * as fs from 'fs'; +import { release } from 'os'; +import { gracefulify } from 'graceful-fs'; +import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp'; +import { StaticRouter, createChannelSender, createChannelReceiver } from 'vs/base/parts/ipc/common/ipc'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import { ExtensionManagementChannel, ExtensionTipsChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -23,24 +24,23 @@ import { IRequestService } from 'vs/platform/request/common/request'; import { RequestService } from 'vs/platform/request/browser/requestService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { combinedAppender, NullTelemetryService, ITelemetryAppender, NullAppender } from 'vs/platform/telemetry/common/telemetryUtils'; -import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; -import { TelemetryAppenderChannel } from 'vs/platform/telemetry/node/telemetryIpc'; -import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; +import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; +import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc'; +import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; -import { ILogService, LogLevel, ILoggerService } from 'vs/platform/log/common/log'; +import { ILogService, ILoggerService, MultiplexLogService, ConsoleLogService } from 'vs/platform/log/common/log'; import { LoggerChannelClient, FollowerLogService } from 'vs/platform/log/common/logIpc'; import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; -import { combinedDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { combinedDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { DownloadService } from 'vs/platform/download/common/downloadService'; import { IDownloadService } from 'vs/platform/download/common/download'; -import { IChannel, IServerChannel, StaticRouter, createChannelSender, createChannelReceiver } from 'vs/base/parts/ipc/common/ipc'; import { NodeCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner'; import { LanguagePackCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner'; import { StorageDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner'; import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner'; -import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { IMainProcessService, MessagePortMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { DiagnosticsService, IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -48,7 +48,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, IUserDataSyncStoreManagementService, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration as registerUserDataSyncConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, IUserDataSyncStoreManagementService, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, UserDataSyncMachinesServiceChannel, UserDataSyncAccountServiceChannel, UserDataSyncStoreManagementServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; @@ -67,146 +67,184 @@ import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-s import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationNotificationServiceChannelClient } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc'; -import { ActiveWindowManager } from 'vs/platform/windows/common/windowTracker'; +import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { ExtensionsStorageSyncService, IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ISharedProcessConfiguration } from 'vs/platform/sharedProcess/node/sharedProcess'; +import { LocalizationsUpdater } from 'vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater'; +import { DeprecatedExtensionsCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner'; +import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; -export interface ISharedProcessConfiguration { - readonly machineId: string; - readonly windowId: number; -} +class SharedProcessMain extends Disposable { -export function startup(configuration: ISharedProcessConfiguration) { - handshake(configuration); -} + private server = this._register(new MessagePortServer()); -interface ISharedProcessInitData { - sharedIPCHandle: string; - args: NativeParsedArgs; - logLevel: LogLevel; - nodeCachedDataDir?: string; - backupWorkspacesPath: string; -} + constructor(private configuration: ISharedProcessConfiguration) { + super(); -const eventPrefix = 'monacoworkbench'; + // Enable gracefulFs + gracefulify(fs); -class MainProcessService implements IMainProcessService { - - constructor( - private server: Server, - private mainRouter: StaticRouter - ) { } - - declare readonly _serviceBrand: undefined; - - getChannel(channelName: string): IChannel { - return this.server.getChannel(channelName, this.mainRouter); + this.registerListeners(); } - registerChannel(channelName: string, channel: IServerChannel): void { - this.server.registerChannel(channelName, channel); + private registerListeners(): void { + + // Dispose on exit + const onExit = () => this.dispose(); + process.once('exit', onExit); + ipcRenderer.once('vscode:electron-main->shared-process=exit', onExit); } -} -async function main(server: Server, initData: ISharedProcessInitData, configuration: ISharedProcessConfiguration): Promise { - const services = new ServiceCollection(); + async open(): Promise { - const disposables = new DisposableStore(); + // Services + const instantiationService = await this.initServices(); - const onExit = () => disposables.dispose(); - process.once('exit', onExit); - ipcRenderer.once('vscode:electron-main->shared-process=exit', onExit); + // Config + registerUserDataSyncConfiguration(); - disposables.add(server); + instantiationService.invokeFunction(accessor => { + const logService = accessor.get(ILogService); - const environmentService = new NativeEnvironmentService(initData.args); + // Log info + logService.trace('sharedProcess configuration', JSON.stringify(this.configuration)); - const mainRouter = new StaticRouter(ctx => ctx === 'main'); - const loggerClient = new LoggerChannelClient(server.getChannel('logger', mainRouter)); - const logService = new FollowerLogService(loggerClient, new SpdLogService('sharedprocess', environmentService.logsPath, initData.logLevel)); - disposables.add(logService); - logService.info('main', JSON.stringify(configuration)); + // Channels + this.initChannels(accessor); - const mainProcessService = new MainProcessService(server, mainRouter); - services.set(IMainProcessService, mainProcessService); + // Error handler + this.registerErrorHandler(logService); + }); - // Files - const fileService = new FileService(logService); - services.set(IFileService, fileService); - disposables.add(fileService); - const diskFileSystemProvider = new DiskFileSystemProvider(logService); - disposables.add(diskFileSystemProvider); - fileService.registerProvider(Schemas.file, diskFileSystemProvider); + // Instantiate Contributions + this._register(combinedDisposable( + new NodeCachedDataCleaner(this.configuration.nodeCachedDataDir), + instantiationService.createInstance(LanguagePackCachedDataCleaner), + instantiationService.createInstance(StorageDataCleaner, this.configuration.backupWorkspacesPath), + instantiationService.createInstance(LogsDataCleaner), + instantiationService.createInstance(LocalizationsUpdater), + instantiationService.createInstance(DeprecatedExtensionsCleaner) + )); + } - // Configuration - const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); - disposables.add(configurationService); - await configurationService.initialize(); - - // Storage - const storageService = new NativeStorageService(new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')), logService, environmentService); - await storageService.initialize(); - services.set(IStorageService, storageService); - disposables.add(toDisposable(() => storageService.flush())); - - services.set(IEnvironmentService, environmentService); - services.set(INativeEnvironmentService, environmentService); - - services.set(IProductService, { _serviceBrand: undefined, ...product }); - services.set(ILogService, logService); - services.set(IConfigurationService, configurationService); - services.set(IRequestService, new SyncDescriptor(RequestService)); - services.set(ILoggerService, new SyncDescriptor(LoggerService)); - - const nativeHostService = createChannelSender(mainProcessService.getChannel('nativeHost'), { context: configuration.windowId }); - services.set(INativeHostService, nativeHostService); - const activeWindowManager = new ActiveWindowManager(nativeHostService); - const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id)); - - services.set(IDownloadService, new SyncDescriptor(DownloadService)); - services.set(IExtensionRecommendationNotificationService, new ExtensionRecommendationNotificationServiceChannelClient(server.getChannel('IExtensionRecommendationNotificationService', activeWindowRouter))); - - const instantiationService = new InstantiationService(services); - - let telemetryService: ITelemetryService; - instantiationService.invokeFunction(accessor => { + private async initServices(): Promise { const services = new ServiceCollection(); + + // Environment + const environmentService = new NativeEnvironmentService(this.configuration.args); + services.set(IEnvironmentService, environmentService); + services.set(INativeEnvironmentService, environmentService); + + // Log + const mainRouter = new StaticRouter(ctx => ctx === 'main'); + const loggerClient = new LoggerChannelClient(this.server.getChannel('logger', mainRouter)); // we only use this for log levels + const multiplexLogger = this._register(new MultiplexLogService([ + this._register(new ConsoleLogService(this.configuration.logLevel)), + this._register(new SpdLogService('sharedprocess', environmentService.logsPath, this.configuration.logLevel)) + ])); + + const logService = this._register(new FollowerLogService(loggerClient, multiplexLogger)); + services.set(ILogService, logService); + + // Main Process + const mainProcessService = new MessagePortMainProcessService(this.server, mainRouter); + services.set(IMainProcessService, mainProcessService); + + // Files + const fileService = this._register(new FileService(logService)); + services.set(IFileService, fileService); + + const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService)); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); + + // Configuration + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); + services.set(IConfigurationService, configurationService); + + await configurationService.initialize(); + + // Storage + const storageService = new NativeStorageService(new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')), logService, environmentService); + services.set(IStorageService, storageService); + + await storageService.initialize(); + this._register(toDisposable(() => storageService.flush())); + + // Product + services.set(IProductService, { _serviceBrand: undefined, ...product }); + + // Request + services.set(IRequestService, new SyncDescriptor(RequestService)); + + // Native Host + const nativeHostService = createChannelSender(mainProcessService.getChannel('nativeHost'), { context: this.configuration.windowId }); + services.set(INativeHostService, nativeHostService); + + // Download + services.set(IDownloadService, new SyncDescriptor(DownloadService)); + + // Extension recommendations + const activeWindowManager = this._register(new ActiveWindowManager(nativeHostService)); + const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id)); + services.set(IExtensionRecommendationNotificationService, new ExtensionRecommendationNotificationServiceChannelClient(this.server.getChannel('extensionRecommendationNotification', activeWindowRouter))); + + // Logger + const loggerService = this._register(new LoggerService(logService, fileService)); + services.set(ILoggerService, loggerService); + + // Telemetry const { appRoot, extensionsPath, extensionDevelopmentLocationURI, isBuilt, installSourcePath } = environmentService; - let telemetryAppender: ITelemetryAppender = NullAppender; + let telemetryService: ITelemetryService; + let telemetryAppender: ITelemetryAppender; if (!extensionDevelopmentLocationURI && !environmentService.disableTelemetry && product.enableTelemetry) { - telemetryAppender = new TelemetryLogAppender(accessor.get(ILoggerService), environmentService); + telemetryAppender = new TelemetryLogAppender(loggerService, environmentService); + + // Application Insights if (product.aiConfig && product.aiConfig.asimovKey && isBuilt) { - const appInsightsAppender = new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey); - disposables.add(toDisposable(() => appInsightsAppender!.flush())); // Ensure the AI appender is disposed so that it flushes remaining data + const appInsightsAppender = new AppInsightsAppender('monacoworkbench', null, product.aiConfig.asimovKey); + this._register(toDisposable(() => appInsightsAppender.flush())); // Ensure the AI appender is disposed so that it flushes remaining data telemetryAppender = combinedAppender(appInsightsAppender, telemetryAppender); } - const config: ITelemetryServiceConfig = { + + telemetryService = new TelemetryService({ appender: telemetryAppender, - commonProperties: resolveCommonProperties(product.commit, product.version, configuration.machineId, product.msftInternalDomains, installSourcePath), + commonProperties: resolveCommonProperties(fileService, release(), process.arch, product.commit, product.version, this.configuration.machineId, product.msftInternalDomains, installSourcePath), sendErrorTelemetry: true, piiPaths: [appRoot, extensionsPath] - }; - - telemetryService = new TelemetryService(config, configurationService); - services.set(ITelemetryService, telemetryService); + }, configurationService); } else { telemetryService = NullTelemetryService; - services.set(ITelemetryService, NullTelemetryService); + telemetryAppender = NullAppender; } - server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(telemetryAppender)); + this.server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(telemetryAppender)); + services.set(ITelemetryService, telemetryService); + + // Extension Management services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + + // Extension Gallery services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); - services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); - services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService)); + + // Extension Tips services.set(IExtensionTipsService, new SyncDescriptor(ExtensionTipsService)); + // Localizations + services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); + + // Diagnostics + services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService)); + + // Settings Sync services.set(IUserDataSyncAccountService, new SyncDescriptor(UserDataSyncAccountService)); services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService)); - services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); + services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(this.server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); services.set(IGlobalExtensionEnablementService, new SyncDescriptor(GlobalExtensionEnablementService)); services.set(IIgnoredExtensionsManagementService, new SyncDescriptor(IgnoredExtensionsManagementService)); services.set(IExtensionsStorageSyncService, new SyncDescriptor(ExtensionsStorageSyncService)); @@ -217,114 +255,86 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IUserDataAutoSyncEnablementService, new SyncDescriptor(UserDataAutoSyncEnablementService)); services.set(IUserDataSyncResourceEnablementService, new SyncDescriptor(UserDataSyncResourceEnablementService)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); - registerConfiguration(); - const instantiationService2 = instantiationService.createChild(services); - - instantiationService2.invokeFunction(accessor => { - - const extensionManagementService = accessor.get(IExtensionManagementService); - const channel = new ExtensionManagementChannel(extensionManagementService, () => null); - server.registerChannel('extensions', channel); - - const localizationsService = accessor.get(ILocalizationsService); - const localizationsChannel = createChannelReceiver(localizationsService); - server.registerChannel('localizations', localizationsChannel); - - const diagnosticsService = accessor.get(IDiagnosticsService); - const diagnosticsChannel = createChannelReceiver(diagnosticsService); - server.registerChannel('diagnostics', diagnosticsChannel); - - const extensionTipsService = accessor.get(IExtensionTipsService); - const extensionTipsChannel = new ExtensionTipsChannel(extensionTipsService); - server.registerChannel('extensionTipsService', extensionTipsChannel); - - const userDataSyncMachinesService = accessor.get(IUserDataSyncMachinesService); - const userDataSyncMachineChannel = new UserDataSyncMachinesServiceChannel(userDataSyncMachinesService); - server.registerChannel('userDataSyncMachines', userDataSyncMachineChannel); - - const authTokenService = accessor.get(IUserDataSyncAccountService); - const authTokenChannel = new UserDataSyncAccountServiceChannel(authTokenService); - server.registerChannel('userDataSyncAccount', authTokenChannel); - - const userDataSyncStoreManagementService = accessor.get(IUserDataSyncStoreManagementService); - const userDataSyncStoreManagementChannel = new UserDataSyncStoreManagementServiceChannel(userDataSyncStoreManagementService); - server.registerChannel('userDataSyncStoreManagement', userDataSyncStoreManagementChannel); - - const userDataSyncService = accessor.get(IUserDataSyncService); - const userDataSyncChannel = new UserDataSyncChannel(server, userDataSyncService, logService); - server.registerChannel('userDataSync', userDataSyncChannel); - - const userDataAutoSync = instantiationService2.createInstance(UserDataAutoSyncService); - const userDataAutoSyncChannel = new UserDataAutoSyncChannel(userDataAutoSync); - server.registerChannel('userDataAutoSync', userDataAutoSyncChannel); - - // clean up deprecated extensions - (extensionManagementService as ExtensionManagementService).removeDeprecatedExtensions(); - // update localizations cache - (localizationsService as LocalizationsService).update(); - // cache clean ups - disposables.add(combinedDisposable( - new NodeCachedDataCleaner(initData.nodeCachedDataDir), - instantiationService2.createInstance(LanguagePackCachedDataCleaner), - instantiationService2.createInstance(StorageDataCleaner, initData.backupWorkspacesPath), - instantiationService2.createInstance(LogsDataCleaner), - userDataAutoSync - )); - disposables.add(extensionManagementService as ExtensionManagementService); - }); - }); -} - -function setupIPC(hook: string): Promise { - function setup(retry: boolean): Promise { - return serve(hook).then(null, err => { - if (!retry || platform.isWindows || err.code !== 'EADDRINUSE') { - return Promise.reject(err); - } - - // should retry, not windows and eaddrinuse - - return connect(hook, '').then( - client => { - // we could connect to a running instance. this is not good, abort - client.dispose(); - return Promise.reject(new Error('There is an instance already running.')); - }, - err => { - // it happens on Linux and OS X that the pipe is left behind - // let's delete it, since we can't connect to it - // and the retry the whole thing - try { - fs.unlinkSync(hook); - } catch (e) { - return Promise.reject(new Error('Error deleting the shared ipc hook.')); - } - - return setup(false); - } - ); - }); + return new InstantiationService(services); } - return setup(true); + private initChannels(accessor: ServicesAccessor): void { + + // Extensions Management + const extensionManagementService = accessor.get(IExtensionManagementService); + const channel = new ExtensionManagementChannel(extensionManagementService, () => null); + this.server.registerChannel('extensions', channel); + + // Localizations + const localizationsService = accessor.get(ILocalizationsService); + const localizationsChannel = createChannelReceiver(localizationsService); + this.server.registerChannel('localizations', localizationsChannel); + + // Diagnostics + const diagnosticsService = accessor.get(IDiagnosticsService); + const diagnosticsChannel = createChannelReceiver(diagnosticsService); + this.server.registerChannel('diagnostics', diagnosticsChannel); + + // Extension Tips + const extensionTipsService = accessor.get(IExtensionTipsService); + const extensionTipsChannel = new ExtensionTipsChannel(extensionTipsService); + this.server.registerChannel('extensionTipsService', extensionTipsChannel); + + // Settings Sync + const userDataSyncMachinesService = accessor.get(IUserDataSyncMachinesService); + const userDataSyncMachineChannel = new UserDataSyncMachinesServiceChannel(userDataSyncMachinesService); + this.server.registerChannel('userDataSyncMachines', userDataSyncMachineChannel); + + const authTokenService = accessor.get(IUserDataSyncAccountService); + const authTokenChannel = new UserDataSyncAccountServiceChannel(authTokenService); + this.server.registerChannel('userDataSyncAccount', authTokenChannel); + + const userDataSyncStoreManagementService = accessor.get(IUserDataSyncStoreManagementService); + const userDataSyncStoreManagementChannel = new UserDataSyncStoreManagementServiceChannel(userDataSyncStoreManagementService); + this.server.registerChannel('userDataSyncStoreManagement', userDataSyncStoreManagementChannel); + + const userDataSyncService = accessor.get(IUserDataSyncService); + const userDataSyncChannel = new UserDataSyncChannel(this.server, userDataSyncService, accessor.get(ILogService)); + this.server.registerChannel('userDataSync', userDataSyncChannel); + + const userDataAutoSync = this._register(accessor.get(IInstantiationService).createInstance(UserDataAutoSyncService)); + const userDataAutoSyncChannel = new UserDataAutoSyncChannel(userDataAutoSync); + this.server.registerChannel('userDataAutoSync', userDataAutoSyncChannel); + } + + private registerErrorHandler(logService: ILogService): void { + + // Listen on unhandled rejection events + window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => { + + // See https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent + onUnexpectedError(event.reason); + + // Prevent the printing of this event to the console + event.preventDefault(); + }); + + // Install handler for unexpected errors + setUnexpectedErrorHandler(error => { + const message = toErrorMessage(error, true); + if (!message) { + return; + } + + logService.error(`[uncaught exception in sharedProcess]: ${message}`); + }); + } } -async function handshake(configuration: ISharedProcessConfiguration): Promise { +export async function main(configuration: ISharedProcessConfiguration): Promise { - // receive payload from electron-main to start things - const data = await new Promise(c => { - ipcRenderer.once('vscode:electron-main->shared-process=payload', (event: unknown, r: ISharedProcessInitData) => c(r)); - - // tell electron-main we are ready to receive payload - ipcRenderer.send('vscode:shared-process->electron-main=ready-for-payload'); - }); - - // await IPC connection and signal this back to electron-main - const server = await setupIPC(data.sharedIPCHandle); + // create shared process and signal back to main that we are + // ready to accept message ports as client connections + const sharedProcess = new SharedProcessMain(configuration); ipcRenderer.send('vscode:shared-process->electron-main=ipc-ready'); // await initialization and signal this back to electron-main - await main(server, data, configuration); + await sharedProcess.open(); ipcRenderer.send('vscode:shared-process->electron-main=init-done'); } diff --git a/src/vs/code/electron-browser/workbench/workbench.html b/src/vs/code/electron-browser/workbench/workbench.html index 40737461d..f36737f2b 100644 --- a/src/vs/code/electron-browser/workbench/workbench.html +++ b/src/vs/code/electron-browser/workbench/workbench.html @@ -3,7 +3,8 @@ - + + diff --git a/src/vs/code/electron-browser/workbench/workbench.js b/src/vs/code/electron-browser/workbench/workbench.js index 14c699461..64082146a 100644 --- a/src/vs/code/electron-browser/workbench/workbench.js +++ b/src/vs/code/electron-browser/workbench/workbench.js @@ -12,8 +12,7 @@ const bootstrapWindow = bootstrapWindowLib(); // Add a perf entry right from the top - const perf = bootstrapWindow.perfLib(); - perf.mark('renderer/started'); + performance.mark('code/didStartRenderer'); // Load workbench main JS, CSS and NLS all in parallel. This is an // optimization to prevent a waterfall of loading to happen, because @@ -24,10 +23,10 @@ 'vs/nls!vs/workbench/workbench.desktop.main', 'vs/css!vs/workbench/workbench.desktop.main' ], - async function (workbench, configuration) { + function (_, configuration) { // Mark start of workbench - perf.mark('didLoadWorkbenchMain'); + performance.mark('code/didLoadWorkbenchMain'); // @ts-ignore return require('vs/workbench/electron-browser/desktop.main').main(configuration); @@ -41,19 +40,34 @@ loaderConfig.recordStats = true; }, beforeRequire: function () { - perf.mark('willLoadWorkbenchMain'); + performance.mark('code/willLoadWorkbenchMain'); } } ); + // add default trustedTypes-policy for logging and to workaround + // lib/platform limitations + window.trustedTypes?.createPolicy('default', { + createHTML(value) { + // see https://github.com/electron/electron/issues/27211 + // Electron webviews use a static innerHTML default value and + // that isn't trusted. We use a default policy to check for the + // exact value of that innerHTML-string and only allow that. + if (value === '') { + return value; + } + throw new Error('UNTRUSTED html usage, default trusted types policy should NEVER be reached'); + // console.trace('UNTRUSTED html usage, default trusted types policy should NEVER be reached'); + // return value; + } + }); //region Helpers /** * @returns {{ - * load: (modules: string[], resultCallback: (result, configuration: object) => any, options: object) => unknown, - * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals'), - * perfLib: () => { mark: (name: string) => void } + * load: (modules: string[], resultCallback: (result, configuration: import('../../../platform/windows/common/windows').INativeWindowConfiguration) => any, options: object) => unknown, + * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals') * }} */ function bootstrapWindowLib() { @@ -67,12 +81,11 @@ * colorScheme: ('light' | 'dark' | 'hc'), * autoDetectHighContrast?: boolean, * extensionDevelopmentPath?: string[], - * folderUri?: object, - * workspace?: object + * workspace?: import('../../../platform/workspaces/common/workspaces').IWorkspaceIdentifier | import('../../../platform/workspaces/common/workspaces').ISingleFolderWorkspaceIdentifier * }} configuration */ function showPartsSplash(configuration) { - perf.mark('willShowPartsSplash'); + performance.mark('code/willShowPartsSplash'); let data; if (typeof configuration.partsSplashPath === 'string') { @@ -147,7 +160,7 @@ splash.appendChild(activityDiv); // part: side bar (only when opening workspace/folder) - if (configuration.folderUri || configuration.workspace) { + if (configuration.workspace) { // folder or workspace -> status bar color, sidebar const sideDiv = document.createElement('div'); sideDiv.setAttribute('style', `position: absolute; height: calc(100% - ${layoutInfo.titleBarHeight}px); top: ${layoutInfo.titleBarHeight}px; ${layoutInfo.sideBarSide}: ${layoutInfo.activityBarWidth}px; width: ${layoutInfo.sideBarWidth}px; background-color: ${colorInfo.sideBarBackground};`); @@ -156,13 +169,13 @@ // part: statusbar const statusDiv = document.createElement('div'); - statusDiv.setAttribute('style', `position: absolute; width: 100%; bottom: 0; left: 0; height: ${layoutInfo.statusBarHeight}px; background-color: ${configuration.folderUri || configuration.workspace ? colorInfo.statusBarBackground : colorInfo.statusBarNoFolderBackground};`); + statusDiv.setAttribute('style', `position: absolute; width: 100%; bottom: 0; left: 0; height: ${layoutInfo.statusBarHeight}px; background-color: ${configuration.workspace ? colorInfo.statusBarBackground : colorInfo.statusBarNoFolderBackground};`); splash.appendChild(statusDiv); document.body.appendChild(splash); } - perf.mark('didShowPartsSplash'); + performance.mark('code/didShowPartsSplash'); } //#endregion diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 759c85bc7..3f85072d5 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -4,17 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { app, ipcMain, systemPreferences, contentTracing, protocol, BrowserWindow, dialog, session } from 'electron'; -import { IProcessEnvironment, isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; +import { release } from 'os'; +import { IProcessEnvironment, isWindows, isMacintosh, isLinux, isLinuxSnap } from 'vs/base/common/platform'; import { WindowsMainService } from 'vs/platform/windows/electron-main/windowsMainService'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { resolveShellEnv } from 'vs/code/node/shellEnv'; import { IUpdateService } from 'vs/platform/update/common/update'; import { UpdateChannel } from 'vs/platform/update/electron-main/updateIpc'; -import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron-main'; -import { Client } from 'vs/base/parts/ipc/common/ipc.net'; -import { Server, connect } from 'vs/base/parts/ipc/node/ipc.net'; +import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc'; +import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron'; +import { Server as NodeIPCServer } from 'vs/base/parts/ipc/node/ipc.net'; +import { Client as MessagePortClient } from 'vs/base/parts/ipc/electron-main/ipc.mp'; import { SharedProcess } from 'vs/code/electron-main/sharedProcess'; import { LaunchMainService, ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -28,19 +29,17 @@ import { IOpenURLOptions, IURLService } from 'vs/platform/url/common/url'; import { URLHandlerChannelClient, URLHandlerRouter } from 'vs/platform/url/common/urlIpc'; import { ITelemetryService, machineIdKey } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; +import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; -import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; -import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc'; +import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; import product from 'vs/platform/product/common/product'; import { ProxyAuthHandler } from 'vs/code/electron-main/auth'; -import { ProxyAuthHandler2 } from 'vs/code/electron-main/auth2'; import { FileProtocolHandler } from 'vs/code/electron-main/protocol'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { URI } from 'vs/base/common/uri'; import { hasWorkspaceFileExtension, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; -import { WorkspacesService } from 'vs/platform/workspaces/electron-main/workspacesService'; +import { WorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { getMachineId } from 'vs/base/node/id'; import { Win32UpdateService } from 'vs/platform/update/electron-main/updateService.win32'; import { LinuxUpdateService } from 'vs/platform/update/electron-main/updateService.linux'; @@ -64,14 +63,13 @@ import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainSe import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { WorkspacesHistoryMainService, IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { NativeURLService } from 'vs/platform/url/common/urlService'; -import { WorkspacesMainService, IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { WorkspacesManagementMainService, IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { statSync } from 'fs'; import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; import { ElectronExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/electron-main/extensionHostDebugIpc'; import { INativeHostMainService, NativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; -import { ISharedProcessMainService, SharedProcessMainService } from 'vs/platform/ipc/electron-main/sharedProcessMainService'; -import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { withNullAsUndefined } from 'vs/base/common/types'; import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels'; import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMainService'; @@ -81,7 +79,7 @@ import { stripComments } from 'vs/base/common/json'; import { generateUuid } from 'vs/base/common/uuid'; import { VSBuffer } from 'vs/base/common/buffer'; import { EncryptionMainService, IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService'; -import { ActiveWindowManager } from 'vs/platform/windows/common/windowTracker'; +import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; import { IKeyboardLayoutMainService, KeyboardLayoutMainService } from 'vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { DisplayMainService, IDisplayMainService } from 'vs/platform/display/electron-main/displayMainService'; @@ -90,6 +88,7 @@ import { isEqualOrParent } from 'vs/base/common/extpath'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust'; import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/extensionUrlTrustService'; +import { once } from 'vs/base/common/functional'; export class CodeApplication extends Disposable { private windowsMainService: IWindowsMainService | undefined; @@ -97,7 +96,7 @@ export class CodeApplication extends Disposable { private nativeHostMainService: INativeHostMainService | undefined; constructor( - private readonly mainIpcServer: Server, + private readonly mainIpcServer: NodeIPCServer, private readonly userEnv: IProcessEnvironment, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService private readonly logService: ILogService, @@ -150,19 +149,19 @@ export class CodeApplication extends Disposable { event.preventDefault(); }); app.on('remote-get-global', (event, sender, module) => { - this.logService.trace(`App#on(remote-get-global): prevented on ${module}`); + this.logService.trace(`app#on(remote-get-global): prevented on ${module}`); event.preventDefault(); }); app.on('remote-get-builtin', (event, sender, module) => { - this.logService.trace(`App#on(remote-get-builtin): prevented on ${module}`); + this.logService.trace(`app#on(remote-get-builtin): prevented on ${module}`); if (module !== 'clipboard') { event.preventDefault(); } }); app.on('remote-get-current-window', event => { - this.logService.trace(`App#on(remote-get-current-window): prevented`); + this.logService.trace(`app#on(remote-get-current-window): prevented`); event.preventDefault(); }); @@ -171,7 +170,7 @@ export class CodeApplication extends Disposable { return; // the driver needs access to web contents } - this.logService.trace(`App#on(remote-get-current-web-contents): prevented`); + this.logService.trace(`app#on(remote-get-current-web-contents): prevented`); event.preventDefault(); }); @@ -271,9 +270,13 @@ export class CodeApplication extends Disposable { //#region Bootstrap IPC Handlers + let slowShellResolveWarningShown = false; ipcMain.on('vscode:fetchShellEnv', async event => { + + // DO NOT remove: not only usual windows are fetching the + // shell environment but also shared process, issue reporter + // etc, so we need to reply via `webContents` always const webContents = event.sender; - const window = this.windowsMainService?.getWindowByWebContents(event.sender); let replied = false; @@ -291,11 +294,19 @@ export class CodeApplication extends Disposable { } // Handle slow shell environment resolve calls: - // - a warning after 3s but continue to resolve - // - an error after 10s and stop trying to resolve + // - a warning after 3s but continue to resolve (only once in active window) + // - an error after 10s and stop trying to resolve (in every window where this happens) const cts = new CancellationTokenSource(); - const shellEnvSlowWarningHandle = setTimeout(() => window?.sendWhenReady('vscode:showShellEnvSlowWarning', cts.token), 3000); - const shellEnvTimeoutErrorHandle = setTimeout(function () { + + const shellEnvSlowWarningHandle = setTimeout(() => { + if (!slowShellResolveWarningShown) { + this.windowsMainService?.sendToFocused('vscode:showShellEnvSlowWarning', cts.token); + slowShellResolveWarningShown = true; + } + }, 3000); + + const window = this.windowsMainService?.getWindowByWebContents(event.sender); // Note: this can be `undefined` for the shared process!! + const shellEnvTimeoutErrorHandle = setTimeout(() => { cts.dispose(true); window?.sendWhenReady('vscode:showShellEnvTimeoutError', CancellationToken.None); acceptShellEnv({}); @@ -304,8 +315,11 @@ export class CodeApplication extends Disposable { // Prefer to use the args and env from the target window // when resolving the shell env. It is possible that // a first window was opened from the UI but a second - // from the CLI and that has implications for wether to + // from the CLI and that has implications for whether to // resolve the shell environment or not. + // + // Window can be undefined for e.g. the shared process + // that is not part of our windows registry! let args: NativeParsedArgs; let env: NodeJS.ProcessEnv; if (window?.config) { @@ -321,10 +335,10 @@ export class CodeApplication extends Disposable { acceptShellEnv(shellEnv); }); - ipcMain.handle('vscode:writeNlsFile', (event, path: unknown, data: unknown) => { + ipcMain.handle('vscode:writeNlsFile', async (event, path: unknown, data: unknown) => { const uri = this.validateNlsPath([path]); if (!uri || typeof data !== 'string') { - return Promise.reject('Invalid operation (vscode:writeNlsFile)'); + throw new Error('Invalid operation (vscode:writeNlsFile)'); } return this.fileService.writeFile(uri, VSBuffer.fromString(data)); @@ -333,7 +347,7 @@ export class CodeApplication extends Disposable { ipcMain.handle('vscode:readNlsFile', async (event, ...paths: unknown[]) => { const uri = this.validateNlsPath(paths); if (!uri) { - return Promise.reject('Invalid operation (vscode:readNlsFile)'); + throw new Error('Invalid operation (vscode:readNlsFile)'); } return (await this.fileService.readFile(uri)).value.toString(); @@ -427,16 +441,20 @@ export class CodeApplication extends Disposable { // Spawn shared process after the first window has opened and 3s have passed const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv); - const sharedProcessClient = sharedProcess.whenIpcReady().then(() => { - this.logService.trace('Shared process: IPC ready'); + const sharedProcessClient = (async () => { + this.logService.trace('Main->SharedProcess#connect'); - return connect(this.environmentService.sharedIPCHandle, 'main'); - }); - const sharedProcessReady = sharedProcess.whenReady().then(() => { - this.logService.trace('Shared process: init ready'); + const port = await sharedProcess.connect(); + + this.logService.trace('Main->SharedProcess#connect: connection established'); + + return new MessagePortClient(port, 'main'); + })(); + const sharedProcessReady = (async () => { + await sharedProcess.whenReady(); return sharedProcessClient; - }); + })(); this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => { this._register(new RunOnceScheduler(async () => { sharedProcess.spawn(await resolveShellEnv(this.logService, this.environmentService.args, process.env)); @@ -454,12 +472,8 @@ export class CodeApplication extends Disposable { this._register(server); } - // Setup Auth Handler (TODO@ben remove old auth handler eventually) - if (this.configurationService.getValue('window.enableExperimentalProxyLoginDialog') === false) { - this._register(new ProxyAuthHandler()); - } else { - this._register(appInstantiationService.createInstance(ProxyAuthHandler2)); - } + // Setup Auth Handler + this._register(appInstantiationService.createInstance(ProxyAuthHandler)); // Open Windows const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient, fileProtocolHandler)); @@ -487,7 +501,7 @@ export class CodeApplication extends Disposable { return machineId; } - private async createServices(machineId: string, sharedProcess: SharedProcess, sharedProcessReady: Promise>): Promise { + private async createServices(machineId: string, sharedProcess: SharedProcess, sharedProcessReady: Promise): Promise { const services = new ServiceCollection(); switch (process.platform) { @@ -496,8 +510,8 @@ export class CodeApplication extends Disposable { break; case 'linux': - if (process.env.SNAP && process.env.SNAP_REVISION) { - services.set(IUpdateService, new SyncDescriptor(SnapUpdateService, [process.env.SNAP, process.env.SNAP_REVISION])); + if (isLinuxSnap) { + services.set(IUpdateService, new SyncDescriptor(SnapUpdateService, [process.env['SNAP'], process.env['SNAP_REVISION']])); } else { services.set(IUpdateService, new SyncDescriptor(LinuxUpdateService)); } @@ -510,7 +524,6 @@ export class CodeApplication extends Disposable { services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, this.userEnv])); services.set(IDialogMainService, new SyncDescriptor(DialogMainService)); - services.set(ISharedProcessMainService, new SyncDescriptor(SharedProcessMainService, [sharedProcess])); services.set(ILaunchMainService, new SyncDescriptor(LaunchMainService)); services.set(IDiagnosticsService, createChannelSender(getDelayedChannel(sharedProcessReady.then(client => client.getChannel('diagnostics'))))); @@ -518,9 +531,9 @@ export class CodeApplication extends Disposable { services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService, [machineId])); services.set(IKeyboardLayoutMainService, new SyncDescriptor(KeyboardLayoutMainService)); services.set(IDisplayMainService, new SyncDescriptor(DisplayMainService)); - services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService)); + services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService, [sharedProcess])); services.set(IWebviewManagerService, new SyncDescriptor(WebviewMainService)); - services.set(IWorkspacesService, new SyncDescriptor(WorkspacesService)); + services.set(IWorkspacesService, new SyncDescriptor(WorkspacesMainService)); services.set(IMenubarMainService, new SyncDescriptor(MenubarMainService)); services.set(IExtensionUrlTrustService, new SyncDescriptor(ExtensionUrlTrustService)); @@ -533,13 +546,13 @@ export class CodeApplication extends Disposable { services.set(IWorkspacesHistoryMainService, new SyncDescriptor(WorkspacesHistoryMainService)); services.set(IURLService, new SyncDescriptor(NativeURLService)); - services.set(IWorkspacesMainService, new SyncDescriptor(WorkspacesMainService)); + services.set(IWorkspacesManagementMainService, new SyncDescriptor(WorkspacesManagementMainService)); // Telemetry if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { const channel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('telemetryAppender'))); const appender = new TelemetryAppenderClient(channel); - const commonProperties = resolveCommonProperties(product.commit, product.version, machineId, product.msftInternalDomains, this.environmentService.installSourcePath); + const commonProperties = resolveCommonProperties(this.fileService, release(), process.arch, product.commit, product.version, machineId, product.msftInternalDomains, this.environmentService.installSourcePath); const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath]; const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths, sendErrorTelemetry: true }; @@ -589,7 +602,7 @@ export class CodeApplication extends Disposable { }); } - private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise>, fileProtocolHandler: FileProtocolHandler): ICodeWindow[] { + private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise, fileProtocolHandler: FileProtocolHandler): ICodeWindow[] { // Register more Main IPC services const launchMainService = accessor.get(ILaunchMainService); @@ -622,10 +635,6 @@ export class CodeApplication extends Disposable { electronIpcServer.registerChannel('nativeHost', nativeHostChannel); sharedProcessClient.then(client => client.registerChannel('nativeHost', nativeHostChannel)); - const sharedProcessMainService = accessor.get(ISharedProcessMainService); - const sharedProcessChannel = createChannelReceiver(sharedProcessMainService); - electronIpcServer.registerChannel('sharedProcess', sharedProcessChannel); - const workspacesService = accessor.get(IWorkspacesService); const workspacesChannel = createChannelReceiver(workspacesService); electronIpcServer.registerChannel('workspaces', workspacesChannel); @@ -705,6 +714,8 @@ export class CodeApplication extends Disposable { }); // Create a URL handler to open file URIs in the active window + // or open new windows. The URL handler will be invoked from + // protocol invocations outside of VSCode. const app = this; const environmentService = this.environmentService; urlService.registerHandler({ @@ -718,13 +729,15 @@ export class CodeApplication extends Disposable { // Check for URIs to open in window const windowOpenableFromProtocolLink = app.getWindowOpenableFromProtocolLink(uri); if (windowOpenableFromProtocolLink) { - windowsMainService.open({ + const [window] = windowsMainService.open({ context: OpenContext.API, cli: { ...environmentService.args }, urisToOpen: [windowOpenableFromProtocolLink], gotoLineMode: true }); + window.focus(); // this should help ensuring that the right window gets focus when multiple are opened + return true; } @@ -832,7 +845,7 @@ export class CodeApplication extends Disposable { mnemonicButtonLabel(localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&No")), ], cancelId: 1, - message: localize('confirmOpenMessage', "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", getPathLabel(uri.fsPath), product.nameShort), + message: localize('confirmOpenMessage', "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", getPathLabel(uri.fsPath, this.environmentService), product.nameShort), detail: localize('confirmOpenDetail', "If you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'No'"), noLink: true }); @@ -901,8 +914,25 @@ export class CodeApplication extends Disposable { // Signal phase: after window open this.lifecycleMainService.phase = LifecycleMainPhase.AfterWindowOpen; + // Windows: install mutex + const win32MutexName = product.win32MutexName; + if (isWindows && win32MutexName) { + try { + const WindowsMutex = (require.__$__nodeRequire('windows-mutex') as typeof import('windows-mutex')).Mutex; + const mutex = new WindowsMutex(win32MutexName); + once(this.lifecycleMainService.onWillShutdown)(() => mutex.release()); + } catch (error) { + this.logService.error(error); + } + } + // Remote Authorities - this.handleRemoteAuthorities(); + protocol.registerHttpProtocol(Schemas.vscodeRemoteResource, (request, callback) => { + callback({ + url: request.url.replace(/^vscode-remote-resource:/, 'http:'), + method: request.method + }); + }); // Initialize update service const updateService = accessor.get(IUpdateService); @@ -934,19 +964,11 @@ export class CodeApplication extends Disposable { '}' ]; const newArgvString = argvString.substring(0, argvString.length - 2).concat(',\n', additionalArgvContent.join('\n')); + await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(newArgvString)); } } catch (error) { this.logService.error(error); } } - - private handleRemoteAuthorities(): void { - protocol.registerHttpProtocol(Schemas.vscodeRemoteResource, (request, callback) => { - callback({ - url: request.url.replace(/^vscode-remote-resource:/, 'http:'), - method: request.method - }); - }); - } } diff --git a/src/vs/code/electron-main/auth.ts b/src/vs/code/electron-main/auth.ts index b40960186..b9669f983 100644 --- a/src/vs/code/electron-main/auth.ts +++ b/src/vs/code/electron-main/auth.ts @@ -3,18 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { Disposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; -import { FileAccess } from 'vs/base/common/network'; -import { BrowserWindow, BrowserWindowConstructorOptions, app, AuthInfo, WebContents, Event as ElectronEvent } from 'electron'; +import { hash } from 'vs/base/common/hash'; +import { app, AuthInfo, WebContents, Event as ElectronEvent, AuthenticationResponseDetails } from 'electron'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; +import { IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService'; +import { generateUuid } from 'vs/base/common/uuid'; +import product from 'vs/platform/product/common/product'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDetails { + firstAuthAttempt?: boolean; // https://github.com/electron/electron/blob/84a42a050e7d45225e69df5bd2d2bf9f1037ea41/shell/browser/login_handler.cc#L70 +} type LoginEvent = { event: ElectronEvent; - webContents: WebContents; - req: Request; authInfo: AuthInfo; - cb: (username: string, password: string) => void; + req: ElectronAuthenticationResponseDetails; + + callback: (username?: string, password?: string) => void; }; type Credentials = { @@ -22,81 +32,211 @@ type Credentials = { password: string; }; +enum ProxyAuthState { + + /** + * Initial state: we will try to use stored credentials + * first to reply to the auth challenge. + */ + Initial = 1, + + /** + * We used stored credentials and are still challenged, + * so we will show a login dialog next. + */ + StoredCredentialsUsed, + + /** + * Finally, if we showed a login dialog already, we will + * not show any more login dialogs until restart to reduce + * the UI noise. + */ + LoginDialogShown +} + export class ProxyAuthHandler extends Disposable { - declare readonly _serviceBrand: undefined; + private static PROXY_CREDENTIALS_SERVICE_KEY = `${product.urlProtocol}.proxy-credentials`; - private retryCount = 0; + private pendingProxyResolve: Promise | undefined = undefined; - constructor() { + private state = ProxyAuthState.Initial; + + private sessionCredentials: Credentials | undefined = undefined; + + constructor( + @ILogService private readonly logService: ILogService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, + @IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService + ) { super(); this.registerListeners(); } private registerListeners(): void { - const onLogin = Event.fromNodeEventEmitter(app, 'login', (event, webContents, req, authInfo, cb) => ({ event, webContents, req, authInfo, cb })); + const onLogin = Event.fromNodeEventEmitter(app, 'login', (event: ElectronEvent, webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: AuthInfo, callback) => ({ event, webContents, req, authInfo, callback })); this._register(onLogin(this.onLogin, this)); } - private onLogin({ event, authInfo, cb }: LoginEvent): void { + private async onLogin({ event, authInfo, req, callback }: LoginEvent): Promise { if (!authInfo.isProxy) { - return; + return; // only for proxy } - if (this.retryCount++ > 1) { - return; + if (!this.pendingProxyResolve && this.state === ProxyAuthState.LoginDialogShown && req.firstAuthAttempt) { + this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown'); + + return; // only one dialog per session at max (except when firstAuthAttempt: false which indicates a login problem) } + // Signal we handle this event on our own, otherwise + // Electron will ignore our provided credentials. event.preventDefault(); - const opts: BrowserWindowConstructorOptions = { - alwaysOnTop: true, - skipTaskbar: true, - resizable: false, - width: 450, - height: 225, - show: true, - title: 'VS Code', - webPreferences: { - preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, - sandbox: true, - contextIsolation: true, - enableWebSQL: false, - enableRemoteModule: false, - spellcheck: false, - devTools: false - } - }; + let credentials: Credentials | undefined = undefined; + if (!this.pendingProxyResolve) { + this.logService.trace('auth#onLogin (proxy) - no pending proxy handling found, starting new'); - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) { - opts.parent = focusedWindow; - opts.modal = true; + this.pendingProxyResolve = this.resolveProxyCredentials(authInfo); + try { + credentials = await this.pendingProxyResolve; + } finally { + this.pendingProxyResolve = undefined; + } + } else { + this.logService.trace('auth#onLogin (proxy) - pending proxy handling found'); + + credentials = await this.pendingProxyResolve; } - const win = new BrowserWindow(opts); - const windowUrl = FileAccess.asBrowserUri('vs/code/electron-sandbox/proxy/auth.html', require); - const proxyUrl = `${authInfo.host}:${authInfo.port}`; - const title = localize('authRequire', "Proxy Authentication Required"); - const message = localize('proxyauth', "The proxy {0} requires authentication.", proxyUrl); + // According to Electron docs, it is fine to call back without + // username or password to signal that the authentication was handled + // by us, even though without having credentials received: + // + // > If `callback` is called without a username or password, the authentication + // > request will be cancelled and the authentication error will be returned to the + // > page. + callback(credentials?.username, credentials?.password); + } - const onWindowClose = () => cb('', ''); - win.on('close', onWindowClose); + private async resolveProxyCredentials(authInfo: AuthInfo): Promise { + this.logService.trace('auth#resolveProxyCredentials (proxy) - enter'); - win.setMenu(null); - win.webContents.on('did-finish-load', () => { - const data = { title, message }; - win.webContents.send('vscode:openProxyAuthDialog', data); - }); - win.webContents.on('ipc-message', (event, channel, credentials: Credentials) => { - if (channel === 'vscode:proxyAuthResponse') { - const { username, password } = credentials; - cb(username, password); - win.removeListener('close', onWindowClose); - win.close(); + try { + const credentials = await this.doResolveProxyCredentials(authInfo); + if (credentials) { + this.logService.trace('auth#resolveProxyCredentials (proxy) - got credentials'); + + return credentials; + } else { + this.logService.trace('auth#resolveProxyCredentials (proxy) - did not get credentials'); } + } finally { + this.logService.trace('auth#resolveProxyCredentials (proxy) - exit'); + } + + return undefined; + } + + private async doResolveProxyCredentials(authInfo: AuthInfo): Promise { + this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo); + + // Compute a hash over the authentication info to be used + // with the credentials store to return the right credentials + // given the properties of the auth request + // (see https://github.com/microsoft/vscode/issues/109497) + const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port })); + + // Find any previously stored credentials + let storedUsername: string | undefined = undefined; + let storedPassword: string | undefined = undefined; + try { + const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); + if (encryptedSerializedProxyCredentials) { + const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials)); + + storedUsername = credentials.username; + storedPassword = credentials.password; + } + } catch (error) { + this.logService.error(error); // handle errors by asking user for login via dialog + } + + // Reply with stored credentials unless we used them already. + // In that case we need to show a login dialog again because + // they seem invalid. + if (this.state !== ProxyAuthState.StoredCredentialsUsed && typeof storedUsername === 'string' && typeof storedPassword === 'string') { + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found stored credentials to use'); + this.state = ProxyAuthState.StoredCredentialsUsed; + + return { username: storedUsername, password: storedPassword }; + } + + // Find suitable window to show dialog: prefer to show it in the + // active window because any other network request will wait on + // the credentials and we want the user to present the dialog. + const window = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); + if (!window) { + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - no opened window found to show dialog in'); + + return undefined; // unexpected + } + + this.logService.trace(`auth#doResolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`); + + // Open proxy dialog + const payload = { + authInfo, + username: this.sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored + password: this.sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored + replyChannel: `vscode:proxyAuthResponse:${generateUuid()}` + }; + window.sendWhenReady('vscode:openProxyAuthenticationDialog', CancellationToken.None, payload); + this.state = ProxyAuthState.LoginDialogShown; + + // Handle reply + const loginDialogCredentials = await new Promise(resolve => { + const proxyAuthResponseHandler = async (event: ElectronEvent, channel: string, reply: Credentials & { remember: boolean } | undefined /* canceled */) => { + if (channel === payload.replyChannel) { + this.logService.trace(`auth#doResolveProxyCredentials - exit - received credentials from window ${window.id}`); + window.win.webContents.off('ipc-message', proxyAuthResponseHandler); + + // We got credentials from the window + if (reply) { + const credentials: Credentials = { username: reply.username, password: reply.password }; + + // Update stored credentials based on `remember` flag + try { + if (reply.remember) { + const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials)); + await this.nativeHostMainService.setPassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash, encryptedSerializedCredentials); + } else { + await this.nativeHostMainService.deletePassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); + } + } catch (error) { + this.logService.error(error); // handle gracefully + } + + resolve({ username: credentials.username, password: credentials.password }); + } + + // We did not get any credentials from the window (e.g. cancelled) + else { + resolve(undefined); + } + } + }; + + window.win.webContents.on('ipc-message', proxyAuthResponseHandler); }); - win.loadURL(windowUrl.toString(true)); + + // Remember credentials for the session in case + // the credentials are wrong and we show the dialog + // again + this.sessionCredentials = loginDialogCredentials; + + return loginDialogCredentials; } } diff --git a/src/vs/code/electron-main/auth2.ts b/src/vs/code/electron-main/auth2.ts deleted file mode 100644 index 1b84d4bc4..000000000 --- a/src/vs/code/electron-main/auth2.ts +++ /dev/null @@ -1,242 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; -import { hash } from 'vs/base/common/hash'; -import { app, AuthInfo, WebContents, Event as ElectronEvent, AuthenticationResponseDetails } from 'electron'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; -import { IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService'; -import { generateUuid } from 'vs/base/common/uuid'; -import product from 'vs/platform/product/common/product'; -import { CancellationToken } from 'vs/base/common/cancellation'; - -interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDetails { - firstAuthAttempt?: boolean; // https://github.com/electron/electron/blob/84a42a050e7d45225e69df5bd2d2bf9f1037ea41/shell/browser/login_handler.cc#L70 -} - -type LoginEvent = { - event: ElectronEvent; - authInfo: AuthInfo; - req: ElectronAuthenticationResponseDetails; - - callback: (username?: string, password?: string) => void; -}; - -type Credentials = { - username: string; - password: string; -}; - -enum ProxyAuthState { - - /** - * Initial state: we will try to use stored credentials - * first to reply to the auth challenge. - */ - Initial = 1, - - /** - * We used stored credentials and are still challenged, - * so we will show a login dialog next. - */ - StoredCredentialsUsed, - - /** - * Finally, if we showed a login dialog already, we will - * not show any more login dialogs until restart to reduce - * the UI noise. - */ - LoginDialogShown -} - -export class ProxyAuthHandler2 extends Disposable { - - private static PROXY_CREDENTIALS_SERVICE_KEY = `${product.urlProtocol}.proxy-credentials`; - - private pendingProxyResolve: Promise | undefined = undefined; - - private state = ProxyAuthState.Initial; - - private sessionCredentials: Credentials | undefined = undefined; - - constructor( - @ILogService private readonly logService: ILogService, - @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, - @IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService - ) { - super(); - - this.registerListeners(); - } - - private registerListeners(): void { - const onLogin = Event.fromNodeEventEmitter(app, 'login', (event: ElectronEvent, webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: AuthInfo, callback) => ({ event, webContents, req, authInfo, callback })); - this._register(onLogin(this.onLogin, this)); - } - - private async onLogin({ event, authInfo, req, callback }: LoginEvent): Promise { - if (!authInfo.isProxy) { - return; // only for proxy - } - - if (!this.pendingProxyResolve && this.state === ProxyAuthState.LoginDialogShown && req.firstAuthAttempt) { - this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown'); - - return; // only one dialog per session at max (except when firstAuthAttempt: false which indicates a login problem) - } - - // Signal we handle this event on our own, otherwise - // Electron will ignore our provided credentials. - event.preventDefault(); - - let credentials: Credentials | undefined = undefined; - if (!this.pendingProxyResolve) { - this.logService.trace('auth#onLogin (proxy) - no pending proxy handling found, starting new'); - - this.pendingProxyResolve = this.resolveProxyCredentials(authInfo); - try { - credentials = await this.pendingProxyResolve; - } finally { - this.pendingProxyResolve = undefined; - } - } else { - this.logService.trace('auth#onLogin (proxy) - pending proxy handling found'); - - credentials = await this.pendingProxyResolve; - } - - // According to Electron docs, it is fine to call back without - // username or password to signal that the authentication was handled - // by us, even though without having credentials received: - // - // > If `callback` is called without a username or password, the authentication - // > request will be cancelled and the authentication error will be returned to the - // > page. - callback(credentials?.username, credentials?.password); - } - - private async resolveProxyCredentials(authInfo: AuthInfo): Promise { - this.logService.trace('auth#resolveProxyCredentials (proxy) - enter'); - - try { - const credentials = await this.doResolveProxyCredentials(authInfo); - if (credentials) { - this.logService.trace('auth#resolveProxyCredentials (proxy) - got credentials'); - - return credentials; - } else { - this.logService.trace('auth#resolveProxyCredentials (proxy) - did not get credentials'); - } - } finally { - this.logService.trace('auth#resolveProxyCredentials (proxy) - exit'); - } - - return undefined; - } - - private async doResolveProxyCredentials(authInfo: AuthInfo): Promise { - this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo); - - // Compute a hash over the authentication info to be used - // with the credentials store to return the right credentials - // given the properties of the auth request - // (see https://github.com/microsoft/vscode/issues/109497) - const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port })); - - // Find any previously stored credentials - let storedUsername: string | undefined = undefined; - let storedPassword: string | undefined = undefined; - try { - const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); - if (encryptedSerializedProxyCredentials) { - const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials)); - - storedUsername = credentials.username; - storedPassword = credentials.password; - } - } catch (error) { - this.logService.error(error); // handle errors by asking user for login via dialog - } - - // Reply with stored credentials unless we used them already. - // In that case we need to show a login dialog again because - // they seem invalid. - if (this.state !== ProxyAuthState.StoredCredentialsUsed && typeof storedUsername === 'string' && typeof storedPassword === 'string') { - this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found stored credentials to use'); - this.state = ProxyAuthState.StoredCredentialsUsed; - - return { username: storedUsername, password: storedPassword }; - } - - // Find suitable window to show dialog: prefer to show it in the - // active window because any other network request will wait on - // the credentials and we want the user to present the dialog. - const window = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); - if (!window) { - this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - no opened window found to show dialog in'); - - return undefined; // unexpected - } - - this.logService.trace(`auth#doResolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`); - - // Open proxy dialog - const payload = { - authInfo, - username: this.sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored - password: this.sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored - replyChannel: `vscode:proxyAuthResponse:${generateUuid()}` - }; - window.sendWhenReady('vscode:openProxyAuthenticationDialog', CancellationToken.None, payload); - this.state = ProxyAuthState.LoginDialogShown; - - // Handle reply - const loginDialogCredentials = await new Promise(resolve => { - const proxyAuthResponseHandler = async (event: ElectronEvent, channel: string, reply: Credentials & { remember: boolean } | undefined /* canceled */) => { - if (channel === payload.replyChannel) { - this.logService.trace(`auth#doResolveProxyCredentials - exit - received credentials from window ${window.id}`); - window.win.webContents.off('ipc-message', proxyAuthResponseHandler); - - // We got credentials from the window - if (reply) { - const credentials: Credentials = { username: reply.username, password: reply.password }; - - // Update stored credentials based on `remember` flag - try { - if (reply.remember) { - const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials)); - await this.nativeHostMainService.setPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash, encryptedSerializedCredentials); - } else { - await this.nativeHostMainService.deletePassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); - } - } catch (error) { - this.logService.error(error); // handle gracefully - } - - resolve({ username: credentials.username, password: credentials.password }); - } - - // We did not get any credentials from the window (e.g. cancelled) - else { - resolve(undefined); - } - } - }; - - window.win.webContents.on('ipc-message', proxyAuthResponseHandler); - }); - - // Remember credentials for the session in case - // the credentials are wrong and we show the dialog - // again - this.sessionCredentials = loginDialogCredentials; - - return loginDialogCredentials; - } -} diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 783ac6fb5..f0d8b6d2d 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -5,15 +5,17 @@ import 'vs/platform/update/common/update.config.contribution'; import { app, dialog } from 'electron'; -import * as fs from 'fs'; +import { unlinkSync } from 'fs'; +import { localize } from 'vs/nls'; import { isWindows, IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import product from 'vs/platform/product/common/product'; import { parseMainProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper'; import { createWaitMarkerFile } from 'vs/platform/environment/node/waitMarkerFile'; import { mkdirp } from 'vs/base/node/pfs'; import { LifecycleMainService, ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; -import { Server, serve, connect, XDG_RUNTIME_DIR } from 'vs/base/parts/ipc/node/ipc.net'; import { createChannelSender } from 'vs/base/parts/ipc/common/ipc'; +import { Server as NodeIPCServer, serve as nodeIPCServe, connect as nodeIPCConnect, XDG_RUNTIME_DIR } from 'vs/base/parts/ipc/node/ipc.net'; +import { Client as NodeIPCClient } from 'vs/base/parts/ipc/common/ipc.net'; import { ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; @@ -29,17 +31,15 @@ import { ConfigurationService } from 'vs/platform/configuration/common/configura import { IRequestService } from 'vs/platform/request/common/request'; import { RequestMainService } from 'vs/platform/request/electron-main/requestMainService'; import { CodeApplication } from 'vs/code/electron-main/app'; -import { localize } from 'vs/nls'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { getPathLabel, mnemonicButtonLabel } from 'vs/base/common/labels'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { IThemeMainService, ThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; -import { Client } from 'vs/base/parts/ipc/common/ipc.net'; import { once } from 'vs/base/common/functional'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/node/signService'; -import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; +import { DiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { FileService } from 'vs/platform/files/common/fileService'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; @@ -53,6 +53,8 @@ import { rtrim, trim } from 'vs/base/common/strings'; import { basename, resolve } from 'vs/base/common/path'; import { coalesce, distinct } from 'vs/base/common/arrays'; import { EnvironmentMainService, IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; class ExpectedError extends Error { readonly isExpected = true; @@ -212,14 +214,14 @@ class CodeMain { return instanceEnvironment; } - private async doStartup(args: NativeParsedArgs, logService: ILogService, environmentService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, retry: boolean): Promise { + private async doStartup(args: NativeParsedArgs, logService: ILogService, environmentService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, retry: boolean): Promise { // Try to setup a server for running. If that succeeds it means // we are the first instance to startup. Otherwise it is likely // that another instance is already running. - let server: Server; + let server: NodeIPCServer; try { - server = await serve(environmentService.mainIPCHandle); + server = await nodeIPCServe(environmentService.mainIPCHandle); once(lifecycleMainService.onWillShutdown)(() => server.dispose()); } catch (error) { @@ -235,9 +237,9 @@ class CodeMain { } // there's a running instance, let's connect to it - let client: Client; + let client: NodeIPCClient; try { - client = await connect(environmentService.mainIPCHandle, 'main'); + client = await nodeIPCConnect(environmentService.mainIPCHandle, 'main'); } catch (error) { // Handle unexpected connection errors by showing a dialog to the user @@ -256,7 +258,7 @@ class CodeMain { // let's delete it, since we can't connect to it and then // retry the whole thing try { - fs.unlinkSync(environmentService.mainIPCHandle); + unlinkSync(environmentService.mainIPCHandle); } catch (error) { logService.warn('Could not delete obsolete instance handle', error); @@ -293,11 +295,7 @@ class CodeMain { // Process Info if (args.status) { return instantiationService.invokeFunction(async () => { - - // Create a diagnostic service connected to the existing shared process - const sharedProcessClient = await connect(environmentService.sharedIPCHandle, 'main'); - const diagnosticsChannel = sharedProcessClient.getChannel('diagnostics'); - const diagnosticsService = createChannelSender(diagnosticsChannel); + const diagnosticsService = new DiagnosticsService(NullTelemetryService); const mainProcessInfo = await launchService.getMainProcessInfo(); const remoteDiagnostics = await launchService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true }); const diagnostics = await diagnosticsService.getDiagnostics(mainProcessInfo, remoteDiagnostics); @@ -343,11 +341,11 @@ class CodeMain { private handleStartupDataDirError(environmentService: IEnvironmentMainService, error: NodeJS.ErrnoException): void { if (error.code === 'EACCES' || error.code === 'EPERM') { - const directories = coalesce([environmentService.userDataPath, environmentService.extensionsPath, XDG_RUNTIME_DIR]); + const directories = coalesce([environmentService.userDataPath, environmentService.extensionsPath, XDG_RUNTIME_DIR]).map(folder => getPathLabel(folder, environmentService)); this.showStartupWarningDialog( localize('startupDataDirError', "Unable to write program user data."), - localize('startupUserDataAndExtensionsDirErrorDetail', "Please make sure the following directories are writeable:\n\n{0}", directories.join('\n')) + localize('startupUserDataAndExtensionsDirErrorDetail', "{0}\n\nPlease make sure the following directories are writeable:\n\n{1}", toErrorMessage(error), directories.join('\n')) ); } } diff --git a/src/vs/code/electron-main/protocol.ts b/src/vs/code/electron-main/protocol.ts index 36d093199..5bfbebb68 100644 --- a/src/vs/code/electron-main/protocol.ts +++ b/src/vs/code/electron-main/protocol.ts @@ -11,7 +11,7 @@ import { INativeEnvironmentService } from 'vs/platform/environment/common/enviro import { session } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; import { TernarySearchTree } from 'vs/base/common/map'; -import { isLinux } from 'vs/base/common/platform'; +import { isLinux, isPreferringBrowserCodeLoad } from 'vs/base/common/platform'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; type ProtocolCallback = { (result: string | Electron.FilePathWithHeaders | { error: number }): void }; @@ -37,15 +37,17 @@ export class FileProtocolHandler extends Disposable { // Register vscode-file:// handler defaultSession.protocol.registerFileProtocol(Schemas.vscodeFileResource, (request, callback) => this.handleResourceRequest(request, callback as unknown as ProtocolCallback)); - // Block any file:// access (sandbox only) - if (environmentService.args.__sandbox) { + // Block any file:// access (explicitly enabled only) + if (isPreferringBrowserCodeLoad) { + this.logService.info(`Intercepting ${Schemas.file}: protocol and blocking it`); + defaultSession.protocol.interceptFileProtocol(Schemas.file, (request, callback) => this.handleFileRequest(request, callback as unknown as ProtocolCallback)); } // Cleanup this._register(toDisposable(() => { defaultSession.protocol.unregisterProtocol(Schemas.vscodeFileResource); - if (environmentService.args.__sandbox) { + if (isPreferringBrowserCodeLoad) { defaultSession.protocol.uninterceptProtocol(Schemas.file); } })); diff --git a/src/vs/code/electron-main/sharedProcess.ts b/src/vs/code/electron-main/sharedProcess.ts index dd721db92..812a4dd1e 100644 --- a/src/vs/code/electron-main/sharedProcess.ts +++ b/src/vs/code/electron-main/sharedProcess.ts @@ -3,25 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { memoize } from 'vs/base/common/decorators'; +import { BrowserWindow, ipcMain, Event, MessagePortMain } from 'electron'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { BrowserWindow, ipcMain, WebContents, Event as ElectronEvent } from 'electron'; -import { ISharedProcess } from 'vs/platform/ipc/electron-main/sharedProcessMainService'; import { Barrier } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; -import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; import { FileAccess } from 'vs/base/common/network'; +import { browserCodeLoadingCacheStrategy } from 'vs/base/common/platform'; +import { ISharedProcess, ISharedProcessConfiguration } from 'vs/platform/sharedProcess/node/sharedProcess'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { connect as connectMessagePort } from 'vs/base/parts/ipc/electron-main/ipc.mp'; +import { assertIsDefined } from 'vs/base/common/types'; -export class SharedProcess implements ISharedProcess { +export class SharedProcess extends Disposable implements ISharedProcess { - private barrier = new Barrier(); + private readonly whenSpawnedBarrier = new Barrier(); - private window: BrowserWindow | null = null; - - private readonly _whenReady: Promise; + private window: BrowserWindow | undefined = undefined; + private windowCloseListener: ((event: Event) => void) | undefined = undefined; constructor( private readonly machineId: string, @@ -31,17 +31,125 @@ export class SharedProcess implements ISharedProcess { @ILogService private readonly logService: ILogService, @IThemeMainService private readonly themeMainService: IThemeMainService ) { - // overall ready promise when shared process signals initialization is done - this._whenReady = new Promise(c => ipcMain.once('vscode:shared-process->electron-main=init-done', () => c(undefined))); + super(); + + this.registerListeners(); } - @memoize - private get _whenIpcReady(): Promise { + private registerListeners(): void { + + // Lifecycle + this._register(this.lifecycleMainService.onWillShutdown(() => this.onWillShutdown())); + + // Shared process connections from workbench windows + ipcMain.on('vscode:createSharedProcessMessageChannel', async (e, nonce: string) => { + this.logService.trace('SharedProcess: on vscode:createSharedProcessMessageChannel'); + + // await the shared process to be overall ready + // we do not just wait for IPC ready because the + // workbench window will communicate directly + await this.whenReady(); + + // connect to the shared process window + const port = await this.connect(); + + // Check back if the requesting window meanwhile closed + // Since shared process is delayed on startup there is + // a chance that the window close before the shared process + // was ready for a connection. + if (e.sender.isDestroyed()) { + return port.close(); + } + + // send the port back to the requesting window + e.sender.postMessage('vscode:createSharedProcessMessageChannelResult', nonce, [port]); + }); + } + + private onWillShutdown(): void { + const window = this.window; + if (!window) { + return; // possibly too early before created + } + + // Signal exit to shared process when shutting down + if (!window.isDestroyed() && !window.webContents.isDestroyed()) { + window.webContents.send('vscode:electron-main->shared-process=exit'); + } + + // Shut the shared process down when we are quitting + // + // Note: because we veto the window close, we must first remove our veto. + // Otherwise the application would never quit because the shared process + // window is refusing to close! + // + if (this.windowCloseListener) { + window.removeListener('close', this.windowCloseListener); + this.windowCloseListener = undefined; + } + + // Electron seems to crash on Windows without this setTimeout :| + setTimeout(() => { + try { + window.close(); + } catch (err) { + // ignore, as electron is already shutting down + } + + this.window = undefined; + }, 0); + } + + private _whenReady: Promise | undefined = undefined; + whenReady(): Promise { + if (!this._whenReady) { + // Overall signal that the shared process window was loaded and + // all services within have been created. + this._whenReady = new Promise(resolve => ipcMain.once('vscode:shared-process->electron-main=init-done', () => { + this.logService.trace('SharedProcess: Overall ready'); + + resolve(); + })); + } + + return this._whenReady; + } + + private _whenIpcReady: Promise | undefined = undefined; + private get whenIpcReady() { + if (!this._whenIpcReady) { + this._whenIpcReady = (async () => { + + // Always wait for `spawn()` + await this.whenSpawnedBarrier.wait(); + + // Create window for shared process + this.createWindow(); + + // Listeners + this.registerWindowListeners(); + + // Wait for window indicating that IPC connections are accepted + await new Promise(resolve => ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => { + this.logService.trace('SharedProcess: IPC ready'); + + resolve(); + })); + })(); + } + + return this._whenIpcReady; + } + + private createWindow(): void { + + // shared process is a hidden window by default this.window = new BrowserWindow({ show: false, backgroundColor: this.themeMainService.getBackgroundColor(), webPreferences: { preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, + v8CacheOptions: browserCodeLoadingCacheStrategy, nodeIntegration: true, enableWebSQL: false, enableRemoteModule: false, @@ -52,118 +160,85 @@ export class SharedProcess implements ISharedProcess { disableBlinkFeatures: 'Auxclick' // do NOT change, allows us to identify this window as shared-process in the process explorer } }); - const config = { - appRoot: this.environmentService.appRoot, + + const config: ISharedProcessConfiguration = { machineId: this.machineId, + windowId: this.window.id, + appRoot: this.environmentService.appRoot, nodeCachedDataDir: this.environmentService.nodeCachedDataDir, + backupWorkspacesPath: this.environmentService.backupWorkspacesPath, userEnv: this.userEnv, - windowId: this.window.id + sharedIPCHandle: this.environmentService.sharedIPCHandle, + args: this.environmentService.args, + logLevel: this.logService.getLevel() }; - const windowUrl = (this.environmentService.sandbox ? - FileAccess._asCodeFileUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require) : - FileAccess.asBrowserUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require)) - .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }); - this.window.loadURL(windowUrl.toString(true)); + // Load with config + this.window.loadURL(FileAccess + .asBrowserUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require) + .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }) + .toString(true) + ); + } - // Prevent the window from dying - const onClose = (e: ElectronEvent) => { + private registerWindowListeners(): void { + if (!this.window) { + return; + } + + // Prevent the window from closing + this.windowCloseListener = (e: Event) => { this.logService.trace('SharedProcess#close prevented'); // We never allow to close the shared process unless we get explicitly disposed() e.preventDefault(); // Still hide the window though if visible - if (this.window && this.window.isVisible()) { + if (this.window?.isVisible()) { this.window.hide(); } }; - this.window.on('close', onClose); + this.window.on('close', this.windowCloseListener); - const disposables = new DisposableStore(); - - this.lifecycleMainService.onWillShutdown(() => { - disposables.dispose(); - - // Shut the shared process down when we are quitting - // - // Note: because we veto the window close, we must first remove our veto. - // Otherwise the application would never quit because the shared process - // window is refusing to close! - // - if (this.window) { - this.window.removeListener('close', onClose); - } - - // Electron seems to crash on Windows without this setTimeout :| - setTimeout(() => { - try { - if (this.window) { - this.window.close(); - } - } catch (err) { - // ignore, as electron is already shutting down - } - - this.window = null; - }, 0); - }); - - return new Promise(c => { - // send payload once shared process is ready to receive it - disposables.add(Event.once(Event.fromNodeEventEmitter(ipcMain, 'vscode:shared-process->electron-main=ready-for-payload', ({ sender }: { sender: WebContents }) => sender))(sender => { - sender.send('vscode:electron-main->shared-process=payload', { - sharedIPCHandle: this.environmentService.sharedIPCHandle, - args: this.environmentService.args, - logLevel: this.logService.getLevel(), - backupWorkspacesPath: this.environmentService.backupWorkspacesPath, - nodeCachedDataDir: this.environmentService.nodeCachedDataDir - }); - - // signal exit to shared process when we get disposed - disposables.add(toDisposable(() => sender.send('vscode:electron-main->shared-process=exit'))); - - // complete IPC-ready promise when shared process signals this to us - ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => c(undefined)); - })); - }); + // Crashes & Unrsponsive & Failed to load + this.window.webContents.on('render-process-gone', (event, details) => this.logService.error(`SharedProcess: crashed (detail: ${details?.reason})`)); + this.window.on('unresponsive', () => this.logService.error('SharedProcess: detected unresponsive window')); + this.window.webContents.on('did-fail-load', (event, errorCode, errorDescription) => this.logService.warn('SharedProcess: failed to load window, ', errorDescription)); } spawn(userEnv: NodeJS.ProcessEnv): void { this.userEnv = { ...this.userEnv, ...userEnv }; - this.barrier.open(); + + // Release barrier + this.whenSpawnedBarrier.open(); } - async whenReady(): Promise { - await this.barrier.wait(); - await this._whenReady; + async connect(): Promise { + + // Wait for shared process being ready to accept connection + await this.whenIpcReady; + + // Connect and return message port + const window = assertIsDefined(this.window); + return connectMessagePort(window); } - async whenIpcReady(): Promise { - await this.barrier.wait(); - await this._whenIpcReady; - } + async toggle(): Promise { - toggle(): void { - if (!this.window || this.window.isVisible()) { - this.hide(); - } else { - this.show(); + // wait for window to be created + await this.whenIpcReady; + + if (!this.window) { + return; // possibly disposed already } - } - show(): void { - if (this.window) { + if (this.window.isVisible()) { + this.window.webContents.closeDevTools(); + this.window.hide(); + } else { this.window.show(); this.window.webContents.openDevTools(); } } - - hide(): void { - if (this.window) { - this.window.webContents.closeDevTools(); - this.window.hide(); - } - } } diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index a9f47be81..6ce9e1dc8 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as os from 'os'; -import * as path from 'vs/base/common/path'; -import * as nls from 'vs/nls'; -import * as perf from 'vs/base/common/performance'; +import { release } from 'os'; +import { join } from 'vs/base/common/path'; +import { localize } from 'vs/nls'; +import { getMarks, mark } from 'vs/base/common/performance'; import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme, Event, Details } from 'electron'; +import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme, Event, RenderProcessGoneDetails } from 'electron'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -18,17 +18,17 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import product from 'vs/platform/product/common/product'; import { WindowMinimumSize, IWindowSettings, MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, zoomLevelToZoomFactor, INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { browserCodeLoadingCacheStrategy, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { ICodeWindow, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; -import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; import { RunOnceScheduler } from 'vs/base/common/async'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; @@ -102,32 +102,32 @@ export class CodeWindow extends Disposable implements ICodeWindow { private hiddenTitleBarStyle: boolean | undefined; private showTimeoutHandle: NodeJS.Timeout | undefined; - private _lastFocusTime: number; - private _readyState: ReadyState; + private _lastFocusTime = -1; + private _readyState = ReadyState.NONE; private windowState: IWindowState; private currentMenuBarVisibility: MenuBarVisibility | undefined; private representedFilename: string | undefined; private documentEdited: boolean | undefined; - private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[]; + private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[] = []; private marketplaceHeadersPromise: Promise; - private readonly touchBarGroups: TouchBarSegmentedControl[]; + private readonly touchBarGroups: TouchBarSegmentedControl[] = []; - private currentHttpProxy?: string; - private currentNoProxy?: string; + private currentHttpProxy: string | undefined = undefined; + private currentNoProxy: string | undefined = undefined; constructor( config: IWindowCreationOptions, @ILogService private readonly logService: ILogService, @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, @IFileService private readonly fileService: IFileService, - @IStorageMainService private readonly storageService: IStorageMainService, + @IStorageMainService storageService: IStorageMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeMainService private readonly themeMainService: IThemeMainService, - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @IBackupMainService private readonly backupMainService: IBackupMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IDialogMainService private readonly dialogMainService: IDialogMainService, @@ -135,11 +135,6 @@ export class CodeWindow extends Disposable implements ICodeWindow { ) { super(); - this.touchBarGroups = []; - this._lastFocusTime = -1; - this._readyState = ReadyState.NONE; - this.whenReadyCallbacks = []; - //#region create browser window { // Load window state @@ -164,6 +159,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { title: product.nameLong, webPreferences: { preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, + v8CacheOptions: browserCodeLoadingCacheStrategy, enableWebSQL: false, enableRemoteModule: false, spellcheck: false, @@ -185,13 +181,17 @@ export class CodeWindow extends Disposable implements ICodeWindow { } }; + if (browserCodeLoadingCacheStrategy) { + this.logService.info(`window#ctor: using vscode-file protocol and V8 cache options: ${browserCodeLoadingCacheStrategy}`); + } + // Apply icon to window // Linux: always // Windows: only when running out of sources, otherwise an icon is set by us on the executable if (isLinux) { - options.icon = path.join(this.environmentService.appRoot, 'resources/linux/code.png'); + options.icon = join(this.environmentService.appRoot, 'resources/linux/code.png'); } else if (isWindows && !this.environmentService.isBuilt) { - options.icon = path.join(this.environmentService.appRoot, 'resources/win32/code_150x150.png'); + options.icon = join(this.environmentService.appRoot, 'resources/win32/code_150x150.png'); } if (isMacintosh && !this.useNativeFullScreen()) { @@ -233,7 +233,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._win.setSheetOffset(22); // offset dialogs by the height of the custom title bar if we have any } - // TODO@Ben (Electron 4 regression): when running on multiple displays where the target display + // TODO@bpasero (Electron 4 regression): when running on multiple displays where the target display // to open the window has a larger resolution than the primary display, the window will not size // correctly unless we set the bounds again (https://github.com/microsoft/vscode/issues/74872) // @@ -275,10 +275,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.createTouchBar(); // Request handling - const that = this; this.marketplaceHeadersPromise = resolveMarketplaceHeaders(product.version, this.environmentService, this.fileService, { - get(key) { return that.storageService.get(key); }, - store(key, value) { that.storageService.store(key, value); } + get(key) { return storageService.get(key); }, + store(key, value) { storageService.store(key, value); } }); // Eventing @@ -361,13 +360,11 @@ export class CodeWindow extends Disposable implements ICodeWindow { get lastFocusTime(): number { return this._lastFocusTime; } - get backupPath(): string | undefined { return this.currentConfig ? this.currentConfig.backupPath : undefined; } + get backupPath(): string | undefined { return this.currentConfig?.backupPath; } - get openedWorkspace(): IWorkspaceIdentifier | undefined { return this.currentConfig ? this.currentConfig.workspace : undefined; } + get openedWorkspace(): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { return this.currentConfig?.workspace; } - get openedFolderUri(): URI | undefined { return this.currentConfig ? this.currentConfig.folderUri : undefined; } - - get remoteAuthority(): string | undefined { return this.currentConfig ? this.currentConfig.remoteAuthority : undefined; } + get remoteAuthority(): string | undefined { return this.currentConfig?.remoteAuthority; } setReady(): void { this._readyState = ReadyState.READY; @@ -413,9 +410,10 @@ export class CodeWindow extends Disposable implements ICodeWindow { private registerListeners(): void { - // Crashes & Unrsponsive + // Crashes & Unrsponsive & Failed to load this._win.webContents.on('render-process-gone', (event, details) => this.onWindowError(WindowError.CRASHED, details)); this._win.on('unresponsive', () => this.onWindowError(WindowError.UNRESPONSIVE)); + this._win.webContents.on('did-fail-load', (event, errorCode, errorDescription) => this.logService.warn('Main: failed to load workbench window, ', errorDescription)); // Window close this._win.on('closed', () => { @@ -424,24 +422,42 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.dispose(); }); - // Prevent loading of svgs - this._win.webContents.session.webRequest.onBeforeRequest(null!, (details, callback) => { - if (details.url.indexOf('.svg') > 0) { - const uri = URI.parse(details.url); - if (uri && !uri.scheme.match(/file/i) && uri.path.endsWith('.svg')) { + const svgFileSchemes = new Set([Schemas.file, Schemas.vscodeFileResource, Schemas.vscodeRemoteResource, 'devtools']); + this._win.webContents.session.webRequest.onBeforeRequest((details, callback) => { + const uri = URI.parse(details.url); + // Prevent loading of remote svgs + if (uri && uri.path.endsWith('.svg')) { + const safeScheme = svgFileSchemes.has(uri.scheme) || + uri.path.includes(Schemas.vscodeRemoteResource); + if (!safeScheme) { return callback({ cancel: true }); } } - return callback({}); + return callback({ cancel: false }); }); - this._win.webContents.session.webRequest.onHeadersReceived(null!, (details, callback) => { + this._win.webContents.session.webRequest.onHeadersReceived((details, callback) => { const responseHeaders = details.responseHeaders as Record; - const contentType = (responseHeaders['content-type'] || responseHeaders['Content-Type']); - if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) { - return callback({ cancel: true }); + + if (contentType && Array.isArray(contentType)) { + const uri = URI.parse(details.url); + // https://github.com/microsoft/vscode/issues/97564 + // ensure local svg files have Content-Type image/svg+xml + if (uri && uri.path.endsWith('.svg')) { + if (svgFileSchemes.has(uri.scheme)) { + responseHeaders['Content-Type'] = ['image/svg+xml']; + return callback({ cancel: false, responseHeaders }); + } + } + + // remote extension schemes have the following format + // http://127.0.0.1:/vscode-remote-resource?path= + if (!uri.path.includes(Schemas.vscodeRemoteResource) && + contentType.some(x => x.toLowerCase().includes('image/svg'))) { + return callback({ cancel: true }); + } } return callback({ cancel: false }); @@ -526,16 +542,11 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.sendWhenReady('vscode:leaveFullScreen', CancellationToken.None); }); - // Window Failed to load - this._win.webContents.on('did-fail-load', (event: Event, errorCode: number, errorDescription: string, validatedURL: string, isMainFrame: boolean) => { - this.logService.warn('[electron event]: fail to load, ', errorDescription); - }); - // Handle configuration changes this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated())); // Handle Workspace events - this._register(this.workspacesMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e))); + this._register(this.workspacesManagementMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e))); // Inject headers when requests are incoming const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; @@ -544,9 +555,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { } private onWindowError(error: WindowError.UNRESPONSIVE): void; - private onWindowError(error: WindowError.CRASHED, details: Details): void; - private onWindowError(error: WindowError, details?: Details): void { - this.logService.error(error === WindowError.CRASHED ? `[VS Code]: renderer process crashed (detail: ${details?.reason})` : '[VS Code]: detected unresponsive'); + private onWindowError(error: WindowError.CRASHED, details: RenderProcessGoneDetails): void; + private onWindowError(error: WindowError, details?: RenderProcessGoneDetails): void { + this.logService.error(error === WindowError.CRASHED ? `Main: renderer process crashed (detail: ${details?.reason})` : 'Main: detected unresponsive'); // If we run extension tests from CLI, showing a dialog is not // very helpful in this case. Rather, we bring down the test run @@ -568,7 +579,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Unresponsive if (error === WindowError.UNRESPONSIVE) { if (this.isExtensionDevelopmentHost || this.isExtensionTestHost || (this._win && this._win.webContents && this._win.webContents.isDevToolsOpened())) { - // TODO@Ben Workaround for https://github.com/microsoft/vscode/issues/56994 + // TODO@bpasero Workaround for https://github.com/microsoft/vscode/issues/56994 // In certain cases the window can report unresponsiveness because a breakpoint was hit // and the process is stopped executing. The most typical cases are: // - devtools are opened and debugging happens @@ -581,9 +592,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.dialogMainService.showMessageBox({ title: product.nameLong, type: 'warning', - buttons: [mnemonicButtonLabel(nls.localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(nls.localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(nls.localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], - message: nls.localize('appStalled', "The window is no longer responding"), - detail: nls.localize('appStalledDetail', "You can reopen or close the window or keep waiting."), + buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], + message: localize('appStalled', "The window is no longer responding"), + detail: localize('appStalledDetail', "You can reopen or close the window or keep waiting."), noLink: true }, this._win).then(result => { if (!this._win) { @@ -591,6 +602,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } if (result.response === 0) { + this._win.webContents.forcefullyCrashRenderer(); // Calling reload() immediately after calling this method will force the reload to occur in a new process this.reload(); } else if (result.response === 2) { this.destroyWindow(); @@ -602,17 +614,17 @@ export class CodeWindow extends Disposable implements ICodeWindow { else { let message: string; if (details && details.reason !== 'crashed') { - message = nls.localize('appCrashedDetails', "The window has crashed (reason: '{0}')", details?.reason); + message = localize('appCrashedDetails', "The window has crashed (reason: '{0}')", details?.reason); } else { - message = nls.localize('appCrashed', "The window has crashed", details?.reason); + message = localize('appCrashed', "The window has crashed", details?.reason); } this.dialogMainService.showMessageBox({ title: product.nameLong, type: 'warning', - buttons: [mnemonicButtonLabel(nls.localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(nls.localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], + buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], message, - detail: nls.localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."), + detail: localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."), noLink: true }, this._win).then(result => { if (!this._win) { @@ -643,31 +655,32 @@ export class CodeWindow extends Disposable implements ICodeWindow { } private onConfigurationUpdated(): void { + + // Menubar const newMenuBarVisibility = this.getMenuBarVisibility(); if (newMenuBarVisibility !== this.currentMenuBarVisibility) { this.currentMenuBarVisibility = newMenuBarVisibility; this.setMenuBarVisibility(newMenuBarVisibility); } - // Do not set to empty configuration at startup if setting is empty to not override configuration through CLI options: - const env = process.env; + + // Proxy let newHttpProxy = (this.configurationService.getValue('http.proxy') || '').trim() - || (env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY || '').trim() // Not standardized. + || (process.env['https_proxy'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['HTTP_PROXY'] || '').trim() // Not standardized. || undefined; + if (newHttpProxy?.endsWith('/')) { newHttpProxy = newHttpProxy.substr(0, newHttpProxy.length - 1); } - const newNoProxy = (env.no_proxy || env.NO_PROXY || '').trim() || undefined; // Not standardized. + + const newNoProxy = (process.env['no_proxy'] || process.env['NO_PROXY'] || '').trim() || undefined; // Not standardized. if ((newHttpProxy || '').indexOf('@') === -1 && (newHttpProxy !== this.currentHttpProxy || newNoProxy !== this.currentNoProxy)) { this.currentHttpProxy = newHttpProxy; this.currentNoProxy = newNoProxy; + const proxyRules = newHttpProxy || ''; const proxyBypassRules = newNoProxy ? `${newNoProxy},` : ''; this.logService.trace(`Setting proxy to '${proxyRules}', bypassing '${proxyBypassRules}'`); - this._win.webContents.session.setProxy({ - proxyRules, - proxyBypassRules, - pacScript: '', - }); + this._win.webContents.session.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); } } @@ -677,7 +690,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } } - load(config: INativeWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void { + load(config: INativeWindowConfiguration, { isReload, disableExtensions }: { isReload?: boolean, disableExtensions?: boolean } = Object.create(null)): void { // If this window was loaded before from the command line // (as indicated by VSCODE_CLI environment), make sure to @@ -689,6 +702,15 @@ export class CodeWindow extends Disposable implements ICodeWindow { config.userEnv = { ...currentUserEnv, ...config.userEnv }; // still allow to override certain environment as passed in } + // If named pipe was instantiated for the crashpad_handler process, reuse the same + // pipe for new app instances connecting to the original app instance. + // Ref: https://github.com/microsoft/vscode/issues/115874 + if (process.env['CHROME_CRASHPAD_PIPE_NAME']) { + Object.assign(config.userEnv, { + CHROME_CRASHPAD_PIPE_NAME: process.env['CHROME_CRASHPAD_PIPE_NAME'] + }); + } + // If this is the first time the window is loaded, we associate the paths // directly with the window because we assume the loading will just work if (this._readyState === ReadyState.NONE) { @@ -728,7 +750,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Load URL - perf.mark('main:loadWindow'); + mark('code/willOpenNewWindow'); this._win.loadURL(this.getUrl(configuration)); // Make window visible if it did not open in N seconds because this indicates an error @@ -747,11 +769,14 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._onLoad.fire(); } - reload(cli?: NativeParsedArgs): void { + async reload(cli?: NativeParsedArgs): Promise { // Copy our current config for reuse const configuration = Object.assign({}, this.currentConfig); + // Validate workspace + configuration.workspace = await this.validateWorkspace(configuration); + // Delete some properties we do not want during reload delete configuration.filesToOpenOrCreate; delete configuration.filesToDiff; @@ -761,17 +786,44 @@ export class CodeWindow extends Disposable implements ICodeWindow { // in extension development mode. These options are all development related. if (this.isExtensionDevelopmentHost && cli) { configuration.verbose = cli.verbose; + configuration.debugId = cli.debugId; configuration['inspect-extensions'] = cli['inspect-extensions']; configuration['inspect-brk-extensions'] = cli['inspect-brk-extensions']; - configuration.debugId = cli.debugId; configuration['extensions-dir'] = cli['extensions-dir']; } configuration.isInitialStartup = false; // since this is a reload // Load config - const disableExtensions = cli ? cli['disable-extensions'] : undefined; - this.load(configuration, true, disableExtensions); + this.load(configuration, { isReload: true, disableExtensions: cli?.['disable-extensions'] }); + } + + private async validateWorkspace(configuration: INativeWindowConfiguration): Promise { + + // Multi folder + if (isWorkspaceIdentifier(configuration.workspace)) { + const configPath = configuration.workspace.configPath; + if (configPath.scheme === Schemas.file) { + const workspaceExists = await this.fileService.exists(configPath); + if (!workspaceExists) { + return undefined; + } + } + } + + // Single folder + else if (isSingleFolderWorkspaceIdentifier(configuration.workspace)) { + const uri = configuration.workspace.uri; + if (uri.scheme === Schemas.file) { + const folderExists = await this.fileService.exists(uri); + if (!folderExists) { + return undefined; + } + } + } + + // Workspace is valid + return configuration.workspace; } private getUrl(windowConfiguration: INativeWindowConfiguration): string { @@ -780,6 +832,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { windowConfiguration.windowId = this._win.id; windowConfiguration.sessionId = `window:${this._win.id}`; windowConfiguration.logLevel = this.logService.getLevel(); + windowConfiguration.logsPath = this.environmentService.logsPath; // Set zoomlevel const windowConfig = this.configurationService.getValue('window'); @@ -803,14 +856,14 @@ export class CodeWindow extends Disposable implements ICodeWindow { windowConfiguration.maximized = this._win.isMaximized(); // Dump Perf Counters - windowConfiguration.perfEntries = perf.exportEntries(); + windowConfiguration.perfMarks = getMarks(); // Parts splash - windowConfiguration.partsSplashPath = path.join(this.environmentService.userDataPath, 'rapid_render.json'); + windowConfiguration.partsSplashPath = join(this.environmentService.userDataPath, 'rapid_render.json'); // OS Info windowConfiguration.os = { - release: os.release() + release: release() }; // Config (combination of process.argv and window configuration) @@ -848,10 +901,10 @@ export class CodeWindow extends Disposable implements ICodeWindow { workbench = 'vs/code/electron-browser/workbench/workbench.html'; } - return (this.environmentService.sandbox ? - FileAccess._asCodeFileUri(workbench, require) : - FileAccess.asBrowserUri(workbench, require)) - .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }).toString(true); + return FileAccess + .asBrowserUri(workbench, require) + .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }) + .toString(true); } serializeWindowState(): IWindowState { @@ -1161,7 +1214,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { if (visibility === 'toggle') { if (notify) { - this.send('vscode:showInfoMessage', nls.localize('hiddenMenuBar', "You can still access the menu bar by pressing the Alt-key.")); + this.send('vscode:showInfoMessage', localize('hiddenMenuBar', "You can still access the menu bar by pressing the Alt-key.")); } } @@ -1256,6 +1309,11 @@ export class CodeWindow extends Disposable implements ICodeWindow { send(channel: string, ...args: any[]): void { if (this._win) { + if (this._win.isDestroyed() || this._win.webContents.isDestroyed()) { + this.logService.warn(`Sending IPC message to channel ${channel} for window that is destroyed`); + return; + } + this._win.webContents.send(channel, ...args); } } diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index 252260a57..6d339e858 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -7,11 +7,10 @@ import 'vs/css!./media/issueReporter'; import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { NativeHostService } from 'vs/platform/native/electron-sandbox/nativeHostService'; -import { ipcRenderer, process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox/window'; import { $, reset, safeInnerHtml, windowOpenNoOpener } from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; -import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; import * as collections from 'vs/base/common/collections'; import { debounce } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -23,9 +22,11 @@ import BaseHtml from 'vs/code/electron-sandbox/issue/issueReporterPage'; import { localize } from 'vs/nls'; import { isRemoteDiagnosticError, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IMainProcessService, MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { IMainProcessService, ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { IssueReporterData, IssueReporterExtensionData, IssueReporterFeatures, IssueReporterStyles, IssueType } from 'vs/platform/issue/common/issue'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; +import { Codicon } from 'vs/base/common/codicons'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; const MAX_URL_LENGTH = 2045; @@ -82,14 +83,12 @@ export class IssueReporter extends Disposable { this.initServices(configuration); - const isSnap = process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION; - const targetExtension = configuration.data.extensionId ? configuration.data.enabledExtensions.find(extension => extension.id === configuration.data.extensionId) : undefined; this.issueReporterModel = new IssueReporterModel({ issueType: configuration.data.issueType || IssueType.Bug, versionInfo: { vscodeVersion: `${configuration.product.nameShort} ${configuration.product.version} (${configuration.product.commit || 'Commit unknown'}, ${configuration.product.date || 'Date unknown'})`, - os: `${this.configuration.os.type} ${this.configuration.os.arch} ${this.configuration.os.release}${isSnap ? ' snap' : ''}` + os: `${this.configuration.os.type} ${this.configuration.os.arch} ${this.configuration.os.release}${platform.isLinuxSnap ? ' snap' : ''}` }, extensionsDisabled: !!configuration.disableExtensions, fileOnExtension: configuration.data.extensionId ? !targetExtension?.isBuiltin : undefined, @@ -267,7 +266,7 @@ export class IssueReporter extends Disposable { private initServices(configuration: IssueReporterConfiguration): void { const serviceCollection = new ServiceCollection(); - const mainProcessService = new MainProcessService(configuration.windowId); + const mainProcessService = new ElectronIPCMainProcessService(configuration.windowId); serviceCollection.set(IMainProcessService, mainProcessService); this.nativeHostService = new NativeHostService(configuration.windowId, mainProcessService) as INativeHostService; @@ -442,7 +441,11 @@ export class IssueReporter extends Disposable { private updatePreviewButtonState() { if (this.isPreviewEnabled()) { - this.previewButton.label = localize('previewOnGitHub', "Preview on GitHub"); + if (this.configuration.data.githubAccessToken) { + this.previewButton.label = localize('createOnGitHub', "Create on GitHub"); + } else { + this.previewButton.label = localize('previewOnGitHub', "Preview on GitHub"); + } this.previewButton.enabled = true; } else { this.previewButton.enabled = false; @@ -598,8 +601,7 @@ export class IssueReporter extends Disposable { issueState = $('span.issue-state'); const issueIcon = $('span.issue-icon'); - const codicon = new CodiconLabel(issueIcon); - codicon.text = issue.state === 'open' ? '$(issue-opened)' : '$(issue-closed)'; + issueIcon.appendChild(renderIcon(issue.state === 'open' ? Codicon.issueOpened : Codicon.issueClosed)); const issueStateLabel = $('span.issue-state.label'); issueStateLabel.textContent = issue.state === 'open' ? localize('open', "Open") : localize('closed', "Closed"); @@ -778,6 +780,35 @@ export class IssueReporter extends Disposable { return isValid; } + private async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string, repositoryName: string }): Promise { + const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`; + const init = { + method: 'POST', + body: JSON.stringify({ + title: issueTitle, + body: issueBody + }), + headers: new Headers({ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.configuration.data.githubAccessToken}` + }) + }; + + return new Promise((resolve, reject) => { + window.fetch(url, init).then((response) => { + if (response.ok) { + response.json().then(result => { + ipcRenderer.send('vscode:openExternal', result.html_url); + ipcRenderer.send('vscode:closeIssueReporter'); + resolve(true); + }); + } else { + resolve(false); + } + }); + }); + } + private async createIssue(): Promise { if (!this.validateInputs()) { // If inputs are invalid, set focus to the first one and add listeners on them @@ -810,8 +841,16 @@ export class IssueReporter extends Disposable { this.hasBeenSubmitted = true; - const baseUrl = this.getIssueUrlWithTitle((this.getElementById('issue-title')).value); + const issueTitle = (this.getElementById('issue-title')).value; const issueBody = this.issueReporterModel.serialize(); + + const issueUrl = this.issueReporterModel.fileOnExtension() ? this.getExtensionGitHubUrl() : this.configuration.product.reportIssueUrl!; + const gitHubDetails = this.parseGitHubUrl(issueUrl); + if (this.configuration.data.githubAccessToken && gitHubDetails) { + return this.submitToGitHub(issueTitle, issueBody, gitHubDetails); + } + + const baseUrl = this.getIssueUrlWithTitle((this.getElementById('issue-title')).value); let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`; if (url.length > MAX_URL_LENGTH) { @@ -841,6 +880,20 @@ export class IssueReporter extends Disposable { }); } + private parseGitHubUrl(url: string): undefined | { repositoryName: string, owner: string } { + // Assumes a GitHub url to a particular repo, https://github.com/repositoryName/owner. + // Repository name and owner cannot contain '/' + const match = /^https?:\/\/github\.com\/([^\/]*)\/([^\/]*).*/.exec(url); + if (match && match.length) { + return { + owner: match[1], + repositoryName: match[2] + }; + } + + return undefined; + } + private getExtensionGitHubUrl(): string { let repositoryUrl = ''; const bugsUrl = this.getExtensionBugsUrl(); diff --git a/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css b/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css index add9cdae2..095663c05 100644 --- a/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css +++ b/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css @@ -49,28 +49,12 @@ body { width: 90px; } -.process-item { - line-height: 22px; +.monaco-list-row:first-of-type { + border-bottom: 1px solid; } -table { - border-collapse: collapse; - width: 100%; - table-layout: fixed; -} - -th[scope='col'] { - vertical-align: bottom; - border-bottom: 1px solid #cccccc; - padding: .5rem; - border-top: 1px solid #cccccc; - cursor: default; -} - -td { - padding: .25rem; - vertical-align: top; - cursor: default; +.row { + display: flex; } .centered { @@ -79,6 +63,9 @@ td { .nameLabel{ text-align: left; + width: calc(100% - 185px); + overflow: hidden; + text-overflow: ellipsis; } .data { @@ -93,15 +80,3 @@ td { padding-left: 20px; white-space: nowrap; } - -tbody > tr:hover { - background-color: #2A2D2E; -} - -.hidden { - display: none; -} - -.header { - display: flex; -} diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer.html b/src/vs/code/electron-sandbox/processExplorer/processExplorer.html index 517805abd..73d89eac3 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.html +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.html @@ -6,7 +6,7 @@ -
    +
    diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts index ef93a1c52..e4e7be122 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts @@ -14,38 +14,226 @@ import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; import { popup } from 'vs/base/parts/contextmenu/electron-sandbox/contextmenu'; import { ProcessItem } from 'vs/base/common/processes'; -import { addDisposableListener, $ } from 'vs/base/browser/dom'; +import * as dom from 'vs/base/browser/dom'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isRemoteDiagnosticError, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; -import { MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; -import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; +import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { ByteSize } from 'vs/platform/files/common/files'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; const DEBUG_FLAGS_PATTERN = /\s--(inspect|debug)(-brk|port)?=(\d+)?/; const DEBUG_PORT_PATTERN = /\s--(inspect|debug)-port=(\d+)/; -interface FormattedProcessItem { - cpu: number; - memory: number; - pid: string; +class ProcessListDelegate implements IListVirtualDelegate { + getHeight(element: MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) { + return 22; + } + + getTemplateId(element: ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) { + if (isProcessItem(element)) { + return 'process'; + } + + if (isMachineProcessInformation(element)) { + return 'machine'; + } + + if (isRemoteDiagnosticError(element)) { + return 'error'; + } + + if (isProcessInformation(element)) { + return 'header'; + } + + return ''; + } +} + +interface IProcessItemTemplateData extends IProcessRowTemplateData { + CPU: HTMLElement; + memory: HTMLElement; + PID: HTMLElement; +} + +interface IProcessRowTemplateData { + name: HTMLElement; +} + +class ProcessTreeDataSource implements IDataSource { + hasChildren(element: ProcessTree | ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError): boolean { + if (isRemoteDiagnosticError(element)) { + return false; + } + + if (isProcessItem(element)) { + return !!element.children?.length; + } else { + return true; + } + } + + getChildren(element: ProcessTree | ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) { + if (isProcessItem(element)) { + return element.children ? element.children : []; + } + + if (isRemoteDiagnosticError(element)) { + return []; + } + + if (isProcessInformation(element)) { + // If there are multiple process roots, return these, otherwise go directly to the root process + if (element.processRoots.length > 1) { + return element.processRoots; + } else { + return [element.processRoots[0].rootProcess]; + } + } + + if (isMachineProcessInformation(element)) { + return [element.rootProcess]; + } + + return [element.processes]; + } +} + +class ProcessHeaderTreeRenderer implements ITreeRenderer { + templateId: string = 'header'; + renderTemplate(container: HTMLElement): IProcessItemTemplateData { + const data = Object.create(null); + const row = dom.append(container, dom.$('.row')); + data.name = dom.append(row, dom.$('.nameLabel')); + data.CPU = dom.append(row, dom.$('.cpu')); + data.memory = dom.append(row, dom.$('.memory')); + data.PID = dom.append(row, dom.$('.pid')); + return data; + } + renderElement(node: ITreeNode, index: number, templateData: IProcessItemTemplateData, height: number | undefined): void { + templateData.name.textContent = localize('name', "Process Name"); + templateData.CPU.textContent = localize('cpu', "CPU %"); + templateData.PID.textContent = localize('pid', "PID"); + templateData.memory.textContent = localize('memory', "Memory (MB)"); + + } + disposeTemplate(templateData: any): void { + // Nothing to do + } +} + +class MachineRenderer implements ITreeRenderer { + templateId: string = 'machine'; + renderTemplate(container: HTMLElement): IProcessRowTemplateData { + const data = Object.create(null); + const row = dom.append(container, dom.$('.row')); + data.name = dom.append(row, dom.$('.nameLabel')); + return data; + } + renderElement(node: ITreeNode, index: number, templateData: IProcessRowTemplateData, height: number | undefined): void { + templateData.name.textContent = node.element.name; + } + disposeTemplate(templateData: IProcessRowTemplateData): void { + // Nothing to do + } +} + +class ErrorRenderer implements ITreeRenderer { + templateId: string = 'error'; + renderTemplate(container: HTMLElement): IProcessRowTemplateData { + const data = Object.create(null); + const row = dom.append(container, dom.$('.row')); + data.name = dom.append(row, dom.$('.nameLabel')); + return data; + } + renderElement(node: ITreeNode, index: number, templateData: IProcessRowTemplateData, height: number | undefined): void { + templateData.name.textContent = node.element.errorMessage; + } + disposeTemplate(templateData: IProcessRowTemplateData): void { + // Nothing to do + } +} + + +class ProcessRenderer implements ITreeRenderer { + constructor(private platform: string, private totalMem: number, private mapPidToWindowTitle: Map) { } + + templateId: string = 'process'; + renderTemplate(container: HTMLElement): IProcessItemTemplateData { + const data = Object.create(null); + const row = dom.append(container, dom.$('.row')); + + data.name = dom.append(row, dom.$('.nameLabel')); + data.CPU = dom.append(row, dom.$('.cpu')); + data.memory = dom.append(row, dom.$('.memory')); + data.PID = dom.append(row, dom.$('.pid')); + + return data; + } + renderElement(node: ITreeNode, index: number, templateData: IProcessItemTemplateData, height: number | undefined): void { + const { element } = node; + + let name = element.name; + if (name === 'window') { + const windowTitle = this.mapPidToWindowTitle.get(element.pid); + name = windowTitle !== undefined ? `${name} (${this.mapPidToWindowTitle.get(element.pid)})` : name; + } + + templateData.name.textContent = name; + templateData.name.title = element.cmd; + + templateData.CPU.textContent = element.load.toFixed(0); + templateData.PID.textContent = element.pid.toFixed(0); + + const memory = this.platform === 'win32' ? element.mem : (this.totalMem * (element.mem / 100)); + templateData.memory.textContent = (memory / ByteSize.MB).toFixed(0); + } + + disposeTemplate(templateData: IProcessItemTemplateData): void { + // Nothing to do + } +} + +interface MachineProcessInformation { name: string; - formattedName: string; - cmd: string; + rootProcess: ProcessItem | IRemoteDiagnosticError +} + +interface ProcessInformation { + processRoots: MachineProcessInformation[]; +} + +interface ProcessTree { + processes: ProcessInformation; +} + +function isMachineProcessInformation(item: any): item is MachineProcessInformation { + return !!item.name && !!item.rootProcess; +} + +function isProcessInformation(item: any): item is ProcessInformation { + return !!item.processRoots; +} + +function isProcessItem(item: any): item is ProcessItem { + return !!item.pid; } class ProcessExplorer { private lastRequestTime: number; - private collapsedStateCache: Map = new Map(); - private mapPidToWindowTitle = new Map(); private listeners = new DisposableStore(); private nativeHostService: INativeHostService; + private tree: DataTree | undefined; + constructor(windowId: number, private data: ProcessExplorerData) { - const mainProcessService = new MainProcessService(windowId); + const mainProcessService = new ElectronIPCMainProcessService(windowId); this.nativeHostService = new NativeHostService(windowId, mainProcessService) as INativeHostService; this.applyStyles(data.styles); @@ -56,8 +244,19 @@ class ProcessExplorer { windows.forEach(window => this.mapPidToWindowTitle.set(window.pid, window.title)); }); - ipcRenderer.on('vscode:listProcessesResponse', (event: unknown, processRoots: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]) => { - this.updateProcessInfo(processRoots); + ipcRenderer.on('vscode:listProcessesResponse', async (event: unknown, processRoots: MachineProcessInformation[]) => { + processRoots.forEach((info, index) => { + if (isProcessItem(info.rootProcess)) { + info.rootProcess.name = index === 0 ? `${this.data.applicationName} main` : 'remote agent'; + } + }); + + if (!this.tree) { + await this.createProcessTree(processRoots); + } else { + this.tree.setInput({ processes: { processRoots } }); + } + this.requestProcessList(0); }); @@ -66,54 +265,58 @@ class ProcessExplorer { ipcRenderer.send('vscode:listProcesses'); } - private getProcessList(rootProcess: ProcessItem, isLocal: boolean, totalMem: number): FormattedProcessItem[] { - const processes: FormattedProcessItem[] = []; - const handledProcesses = new Set(); - - if (rootProcess) { - this.getProcessItem(processes, rootProcess, 0, isLocal, totalMem, handledProcesses); + private async createProcessTree(processRoots: MachineProcessInformation[]): Promise { + const container = document.getElementById('process-list'); + if (!container) { + return; } - return processes; - } + const { totalmem } = await this.nativeHostService.getOSStatistics(); - private getProcessItem(processes: FormattedProcessItem[], item: ProcessItem, indent: number, isLocal: boolean, totalMem: number, handledProcesses: Set): void { - const isRoot = (indent === 0); + const renderers = [ + new ProcessRenderer(this.data.platform, totalmem, this.mapPidToWindowTitle), + new ProcessHeaderTreeRenderer(), + new MachineRenderer(), + new ErrorRenderer() + ]; - handledProcesses.add(item.pid); + this.tree = new DataTree('processExplorer', + container, + new ProcessListDelegate(), + renderers, + new ProcessTreeDataSource(), + { + identityProvider: + { + getId: (element: ProcessTree | ProcessItem | MachineProcessInformation | ProcessInformation | IRemoteDiagnosticError) => { + if (isProcessItem(element)) { + return element.pid.toString(); + } - let name = item.name; - if (isRoot) { - name = isLocal ? `${this.data.applicationName} main` : 'remote agent'; - } + if (isRemoteDiagnosticError(element)) { + return element.hostName; + } - if (name === 'window') { - const windowTitle = this.mapPidToWindowTitle.get(item.pid); - name = windowTitle !== undefined ? `${name} (${this.mapPidToWindowTitle.get(item.pid)})` : name; - } + if (isProcessInformation(element)) { + return 'processes'; + } - // Format name with indent - const formattedName = isRoot ? name : `${' '.repeat(indent)} ${name}`; - const memory = this.data.platform === 'win32' ? item.mem : (totalMem * (item.mem / 100)); - processes.push({ - cpu: item.load, - memory: (memory / ByteSize.MB), - pid: item.pid.toFixed(0), - name, - formattedName, - cmd: item.cmd - }); + if (isMachineProcessInformation(element)) { + return element.name; + } - // Recurse into children if any - if (Array.isArray(item.children)) { - item.children.forEach(child => { - if (!child || handledProcesses.has(child.pid)) { - return; // prevent loops + return 'header'; + } } - - this.getProcessItem(processes, child, indent + 1, isLocal, totalMem, handledProcesses); }); - } + + this.tree.setInput({ processes: { processRoots } }); + this.tree.layout(window.innerHeight, window.innerWidth); + this.tree.onContextMenu(e => { + if (isProcessItem(e.element)) { + this.showContextMenu(e.element, true); + } + }); } private isDebuggable(cmd: string): boolean { @@ -121,7 +324,7 @@ class ProcessExplorer { return (matches && matches.length >= 2) || cmd.indexOf('node ') >= 0 || cmd.indexOf('node.exe') >= 0; } - private attachTo(item: FormattedProcessItem) { + private attachTo(item: ProcessItem) { const config: any = { type: 'node', request: 'attach', @@ -150,179 +353,16 @@ class ProcessExplorer { ipcRenderer.send('vscode:workbenchCommand', { id: 'debug.startFromConfig', from: 'processExplorer', args: [config] }); } - private getProcessIdWithHighestProperty(processList: any[], propertyName: string) { - let max = 0; - let maxProcessId; - processList.forEach(process => { - if (process[propertyName] > max) { - max = process[propertyName]; - maxProcessId = process.pid; - } - }); - - return maxProcessId; - } - - private updateSectionCollapsedState(shouldExpand: boolean, body: HTMLElement, twistie: CodiconLabel, sectionName: string) { - if (shouldExpand) { - body.classList.remove('hidden'); - this.collapsedStateCache.set(sectionName, false); - twistie.text = '$(chevron-down)'; - } else { - body.classList.add('hidden'); - this.collapsedStateCache.set(sectionName, true); - twistie.text = '$(chevron-right)'; - } - } - - private renderProcessFetchError(sectionName: string, errorMessage: string) { - const container = document.getElementById('process-list'); - if (!container) { - return; - } - - const body = document.createElement('tbody'); - - this.renderProcessGroupHeader(sectionName, body, container); - - const errorRow = document.createElement('tr'); - const data = document.createElement('td'); - data.textContent = errorMessage; - data.className = 'error'; - data.colSpan = 4; - errorRow.appendChild(data); - - body.appendChild(errorRow); - container.appendChild(body); - } - - private renderProcessGroupHeader(sectionName: string, body: HTMLElement, container: HTMLElement) { - const headerRow = document.createElement('tr'); - - const headerData = document.createElement('td'); - headerData.colSpan = 4; - headerRow.appendChild(headerData); - - const headerContainer = document.createElement('div'); - headerContainer.className = 'header'; - headerData.appendChild(headerContainer); - - const twistieContainer = document.createElement('div'); - const twistieCodicon = new CodiconLabel(twistieContainer); - this.updateSectionCollapsedState(!this.collapsedStateCache.get(sectionName), body, twistieCodicon, sectionName); - headerContainer.appendChild(twistieContainer); - - const headerLabel = document.createElement('span'); - headerLabel.textContent = sectionName; - headerContainer.appendChild(headerLabel); - - this.listeners.add(addDisposableListener(headerData, 'click', (e) => { - const isHidden = body.classList.contains('hidden'); - this.updateSectionCollapsedState(isHidden, body, twistieCodicon, sectionName); - })); - - container.appendChild(headerRow); - } - - private renderTableSection(sectionName: string, processList: FormattedProcessItem[], renderManySections: boolean, sectionIsLocal: boolean): void { - const container = document.getElementById('process-list'); - if (!container) { - return; - } - - const highestCPUProcess = this.getProcessIdWithHighestProperty(processList, 'cpu'); - const highestMemoryProcess = this.getProcessIdWithHighestProperty(processList, 'memory'); - - const body = document.createElement('tbody'); - - if (renderManySections) { - this.renderProcessGroupHeader(sectionName, body, container); - } - - processList.forEach(p => { - const row = document.createElement('tr'); - row.id = p.pid.toString(); - - const cpu = document.createElement('td'); - p.pid === highestCPUProcess - ? cpu.classList.add('centered', 'highest') - : cpu.classList.add('centered'); - cpu.textContent = p.cpu.toFixed(0); - - const memory = document.createElement('td'); - p.pid === highestMemoryProcess - ? memory.classList.add('centered', 'highest') - : memory.classList.add('centered'); - memory.textContent = p.memory.toFixed(0); - - const pid = document.createElement('td'); - pid.classList.add('centered'); - pid.textContent = p.pid; - - const name = document.createElement('th'); - name.scope = 'row'; - name.classList.add('data'); - name.title = p.cmd; - name.textContent = p.formattedName; - - row.append(cpu, memory, pid, name); - - this.listeners.add(addDisposableListener(row, 'contextmenu', (e) => { - this.showContextMenu(e, p, sectionIsLocal); - })); - - body.appendChild(row); - }); - - container.appendChild(body); - } - - private async updateProcessInfo(processLists: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]): Promise { - const container = document.getElementById('process-list'); - if (!container) { - return; - } - - container.innerText = ''; - this.listeners.clear(); - - const tableHead = $('thead', undefined); - const row = $('tr'); - tableHead.append(row); - - row.append($('th.cpu', { scope: 'col' }, localize('cpu', "CPU %"))); - row.append($('th.memory', { scope: 'col' }, localize('memory', "Memory (MB)"))); - row.append($('th.pid', { scope: 'col' }, localize('pid', "PID"))); - row.append($('th.nameLabel', { scope: 'col' }, localize('name', "Name"))); - - container.append(tableHead); - - const hasMultipleMachines = Object.keys(processLists).length > 1; - const { totalmem } = await this.nativeHostService.getOSStatistics(); - processLists.forEach((remote, i) => { - const isLocal = i === 0; - if (isRemoteDiagnosticError(remote.rootProcess)) { - this.renderProcessFetchError(remote.name, remote.rootProcess.errorMessage); - } else { - this.renderTableSection(remote.name, this.getProcessList(remote.rootProcess, isLocal, totalmem), hasMultipleMachines, isLocal); - } - }); - } - private applyStyles(styles: ProcessExplorerStyles): void { const styleTag = document.createElement('style'); const content: string[] = []; if (styles.hoverBackground) { - content.push(`tbody > tr:hover, table > tr:hover { background-color: ${styles.hoverBackground}; }`); + content.push(`.monaco-list-row:hover { background-color: ${styles.hoverBackground}; }`); } if (styles.hoverForeground) { - content.push(`tbody > tr:hover, table > tr:hover { color: ${styles.hoverForeground}; }`); - } - - if (styles.highlightForeground) { - content.push(`.highest { color: ${styles.highlightForeground}; }`); + content.push(`.monaco-list-row:hover { color: ${styles.hoverForeground}; }`); } styleTag.textContent = content.join('\n'); @@ -334,9 +374,7 @@ class ProcessExplorer { } } - private showContextMenu(e: MouseEvent, item: FormattedProcessItem, isLocal: boolean) { - e.preventDefault(); - + private showContextMenu(item: ProcessItem, isLocal: boolean) { const items: IContextMenuItem[] = []; const pid = Number(item.pid); @@ -417,8 +455,6 @@ class ProcessExplorer { } } - - export function startup(windowId: number, data: ProcessExplorerData): void { const platformClass = data.platform === 'win32' ? 'windows' : data.platform === 'linux' ? 'linux' : 'mac'; document.body.classList.add(platformClass); // used by our fonts diff --git a/src/vs/code/electron-sandbox/proxy/auth.html b/src/vs/code/electron-sandbox/proxy/auth.html deleted file mode 100644 index 788b68fce..000000000 --- a/src/vs/code/electron-sandbox/proxy/auth.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - -

    -
    -

    -
    -

    -

    -

    - - -

    -
    -
    - - - - - diff --git a/src/vs/code/electron-sandbox/proxy/auth.js b/src/vs/code/electron-sandbox/proxy/auth.js deleted file mode 100644 index 5e0db3c2d..000000000 --- a/src/vs/code/electron-sandbox/proxy/auth.js +++ /dev/null @@ -1,48 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const { ipcRenderer } = window.vscode; - -function promptForCredentials(data) { - return new Promise((c, e) => { - const $title = document.getElementById('title'); - const $username = document.getElementById('username'); - const $password = document.getElementById('password'); - const $form = document.getElementById('form'); - const $cancel = document.getElementById('cancel'); - const $message = document.getElementById('message'); - - function submit() { - c({ username: $username.value, password: $password.value }); - return false; - } - - function cancel() { - c({ username: '', password: '' }); - return false; - } - - $form.addEventListener('submit', submit); - $cancel.addEventListener('click', cancel); - - document.body.addEventListener('keydown', function (e) { - switch (e.keyCode) { - case 27: e.preventDefault(); e.stopPropagation(); return cancel(); - case 13: e.preventDefault(); e.stopPropagation(); return submit(); - } - }); - - $title.textContent = data.title; - $message.textContent = data.message; - $username.focus(); - }); -} - -ipcRenderer.on('vscode:openProxyAuthDialog', async (event, data) => { - const response = await promptForCredentials(data); - ipcRenderer.send('vscode:proxyAuthResponse', response); -}); diff --git a/src/vs/code/electron-sandbox/workbench/workbench.html b/src/vs/code/electron-sandbox/workbench/workbench.html index 40737461d..f36737f2b 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.html +++ b/src/vs/code/electron-sandbox/workbench/workbench.html @@ -3,7 +3,8 @@ - + + diff --git a/src/vs/code/electron-sandbox/workbench/workbench.js b/src/vs/code/electron-sandbox/workbench/workbench.js index 6a46ef2b0..c5cb1debb 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.js +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -12,8 +12,7 @@ const bootstrapWindow = bootstrapWindowLib(); // Add a perf entry right from the top - const perf = bootstrapWindow.perfLib(); - perf.mark('renderer/started'); + performance.mark('code/didStartRenderer'); // Load workbench main JS, CSS and NLS all in parallel. This is an // optimization to prevent a waterfall of loading to happen, because @@ -24,10 +23,10 @@ 'vs/nls!vs/workbench/workbench.desktop.main', 'vs/css!vs/workbench/workbench.desktop.main' ], - async function (workbench, configuration) { + function (_, configuration) { // Mark start of workbench - perf.mark('didLoadWorkbenchMain'); + performance.mark('code/didLoadWorkbenchMain'); // @ts-ignore return require('vs/workbench/electron-sandbox/desktop.main').main(configuration); @@ -41,19 +40,34 @@ loaderConfig.recordStats = true; }, beforeRequire: function () { - perf.mark('willLoadWorkbenchMain'); + performance.mark('code/willLoadWorkbenchMain'); } } ); + // add default trustedTypes-policy for logging and to workaround + // lib/platform limitations + window.trustedTypes?.createPolicy('default', { + createHTML(value) { + // see https://github.com/electron/electron/issues/27211 + // Electron webviews use a static innerHTML default value and + // that isn't trusted. We use a default policy to check for the + // exact value of that innerHTML-string and only allow that. + if (value === '') { + return value; + } + throw new Error('UNTRUSTED html usage, default trusted types policy should NEVER be reached'); + // console.trace('UNTRUSTED html usage, default trusted types policy should NEVER be reached'); + // return value; + } + }); //region Helpers /** * @returns {{ - * load: (modules: string[], resultCallback: (result, configuration: object) => any, options: object) => unknown, - * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals'), - * perfLib: () => { mark: (name: string) => void } + * load: (modules: string[], resultCallback: (result, configuration: import('../../../platform/windows/common/windows').INativeWindowConfiguration) => any, options: object) => unknown, + * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals') * }} */ function bootstrapWindowLib() { diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index c6415a551..ac5570189 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as os from 'os'; -import * as fs from 'fs'; +import { homedir } from 'os'; +import { constants, existsSync, statSync, unlinkSync, chmodSync, truncateSync, readFileSync } from 'fs'; import { spawn, ChildProcess, SpawnOptions } from 'child_process'; import { buildHelpMessage, buildVersionMessage, OPTIONS } from 'vs/platform/environment/node/argv'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { parseCLIProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper'; import { createWaitMarkerFile } from 'vs/platform/environment/node/waitMarkerFile'; import product from 'vs/platform/product/common/product'; -import * as paths from 'vs/base/common/path'; +import { isAbsolute, join } from 'vs/base/common/path'; import { whenDeleted, writeFileSync } from 'vs/base/node/pfs'; import { findFreePort, randomPort } from 'vs/base/node/ports'; import { isWindows, isLinux } from 'vs/base/common/platform'; @@ -69,10 +69,10 @@ export async function main(argv: string[]): Promise { // Validate if ( - !source || !target || source === target || // make sure source and target are provided and are not the same - !paths.isAbsolute(source) || !paths.isAbsolute(target) || // make sure both source and target are absolute paths - !fs.existsSync(source) || !fs.statSync(source).isFile() || // make sure source exists as file - !fs.existsSync(target) || !fs.statSync(target).isFile() // make sure target exists as file + !source || !target || source === target || // make sure source and target are provided and are not the same + !isAbsolute(source) || !isAbsolute(target) || // make sure both source and target are absolute paths + !existsSync(source) || !statSync(source).isFile() || // make sure source exists as file + !existsSync(target) || !statSync(target).isFile() // make sure target exists as file ) { throw new Error('Using --file-write with invalid arguments.'); } @@ -83,15 +83,15 @@ export async function main(argv: string[]): Promise { let targetMode: number = 0; let restoreMode = false; if (!!args['file-chmod']) { - targetMode = fs.statSync(target).mode; - if (!(targetMode & 128) /* readonly */) { - fs.chmodSync(target, targetMode | 128); + targetMode = statSync(target).mode; + if (!(targetMode & constants.S_IWUSR)) { + chmodSync(target, targetMode | constants.S_IWUSR); restoreMode = true; } } // Write source to target - const data = fs.readFileSync(source); + const data = readFileSync(source); if (isWindows) { // On Windows we use a different strategy of saving the file // by first truncating the file and then writing with r+ mode. @@ -99,7 +99,7 @@ export async function main(argv: string[]): Promise { // (see https://github.com/microsoft/vscode/issues/931) and // prevent removing alternate data streams // (see https://github.com/microsoft/vscode/issues/6363) - fs.truncateSync(target, 0); + truncateSync(target, 0); writeFileSync(target, data, { flag: 'r+' }); } else { writeFileSync(target, data); @@ -107,7 +107,7 @@ export async function main(argv: string[]): Promise { // Restore previous mode as needed if (restoreMode) { - fs.chmodSync(target, targetMode); + chmodSync(target, targetMode); } } catch (error) { error.message = `Error using --file-write: ${error.message}`; @@ -215,7 +215,7 @@ export async function main(argv: string[]): Promise { throw new Error('Failed to find free ports for profiler. Make sure to shutdown all instances of the editor first.'); } - const filenamePrefix = paths.join(os.homedir(), 'prof-' + Math.random().toString(16).slice(-4)); + const filenamePrefix = join(homedir(), 'prof-' + Math.random().toString(16).slice(-4)); addArg(argv, `--inspect-brk=${portMain}`); addArg(argv, `--remote-debugging-port=${portRenderer}`); @@ -338,7 +338,7 @@ export async function main(argv: string[]): Promise { // Make sure to delete the tmp stdin file if we have any if (stdinFilePath) { - fs.unlinkSync(stdinFilePath); + unlinkSync(stdinFilePath); } }); } diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 84fa06d76..ee189f832 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; +import { release } from 'os'; +import * as fs from 'fs'; +import { gracefulify } from 'graceful-fs'; +import { isAbsolute, join } from 'vs/base/common/path'; import { raceTimeout } from 'vs/base/common/async'; -import * as semver from 'vs/base/common/semver/semver'; import product from 'vs/platform/product/common/product'; -import * as path from 'vs/base/common/path'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -15,416 +16,147 @@ import { InstantiationService } from 'vs/platform/instantiation/common/instantia import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; -import { IExtensionManagementService, IExtensionGalleryService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IExtensionGalleryService, IExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; -import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; +import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; import { IRequestService } from 'vs/platform/request/common/request'; import { RequestService } from 'vs/platform/request/node/requestService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; import { mkdirp, writeFile } from 'vs/base/node/pfs'; -import { getBaseLabel } from 'vs/base/common/labels'; import { IStateService } from 'vs/platform/state/node/state'; import { StateService } from 'vs/platform/state/node/stateService'; import { ILogService, getLogLevel, LogLevel, ConsoleLogService, MultiplexLogService } from 'vs/platform/log/common/log'; -import { isPromiseCanceledError } from 'vs/base/common/errors'; -import { areSameExtensions, adoptToGalleryExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { URI } from 'vs/base/common/uri'; -import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; -import { IExtensionManifest, ExtensionType, isLanguagePackExtension, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; import { Schemas } from 'vs/base/common/network'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { buildTelemetryMessage } from 'vs/platform/telemetry/node/telemetry'; import { FileService } from 'vs/platform/files/common/fileService'; import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IProductService } from 'vs/platform/product/common/productService'; +import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; +import { URI } from 'vs/base/common/uri'; +import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; -const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id); -const notInstalled = (id: string) => localize('notInstalled', "Extension '{0}' is not installed.", id); -const useId = localize('useId', "Make sure you use the full extension ID, including the publisher, e.g.: {0}", 'ms-dotnettools.csharp'); - -function getId(manifest: IExtensionManifest, withVersion?: boolean): string { - if (withVersion) { - return `${manifest.publisher}.${manifest.name}@${manifest.version}`; - } else { - return `${manifest.publisher}.${manifest.name}`; - } -} - -const EXTENSION_ID_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/; - -export function getIdAndVersion(id: string): [string, string | undefined] { - const matches = EXTENSION_ID_REGEX.exec(id); - if (matches && matches[1]) { - return [adoptToGalleryExtensionId(matches[1]), matches[2]]; - } - return [adoptToGalleryExtensionId(id), undefined]; -} - -type InstallExtensionInfo = { id: string, version?: string, installOptions: InstallOptions }; - -export class Main { +class CliMain extends Disposable { constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, - @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, - ) { } + private argv: NativeParsedArgs + ) { + super(); - async run(argv: NativeParsedArgs): Promise { - if (argv['install-source']) { - await this.setInstallSource(argv['install-source']); - } else if (argv['list-extensions']) { - await this.listExtensions(!!argv['show-versions'], argv['category']); - } else if (argv['install-extension'] || argv['install-builtin-extension']) { - await this.installExtensions(argv['install-extension'] || [], argv['install-builtin-extension'] || [], !!argv['do-not-sync'], !!argv['force']); - } else if (argv['uninstall-extension']) { - await this.uninstallExtension(argv['uninstall-extension'], !!argv['force']); - } else if (argv['locate-extension']) { - await this.locateExtension(argv['locate-extension']); - } else if (argv['telemetry']) { - console.log(buildTelemetryMessage(this.environmentService.appRoot, this.environmentService.extensionsPath)); - } + // Enable gracefulFs + gracefulify(fs); + + this.registerListeners(); } - private setInstallSource(installSource: string): Promise { - return writeFile(this.environmentService.installSourcePath, installSource.slice(0, 30)); + private registerListeners(): void { + + // Dispose on exit + process.once('exit', () => this.dispose()); } - private async listExtensions(showVersions: boolean, category?: string): Promise { - let extensions = await this.extensionManagementService.getInstalled(ExtensionType.User); - const categories = EXTENSION_CATEGORIES.map(c => c.toLowerCase()); - if (category && category !== '') { - if (categories.indexOf(category.toLowerCase()) < 0) { - console.log('Invalid category please enter a valid category. To list valid categories run --category without a category specified'); - return; - } - extensions = extensions.filter(e => { - if (e.manifest.categories) { - const lowerCaseCategories: string[] = e.manifest.categories.map(c => c.toLowerCase()); - return lowerCaseCategories.indexOf(category.toLowerCase()) > -1; - } - return false; - }); - } else if (category === '') { - console.log('Possible Categories: '); - categories.forEach(category => { - console.log(category); - }); - return; - } - extensions.forEach(e => console.log(getId(e.manifest, showVersions))); - } + async run(): Promise { - async installExtensions(extensions: string[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean): Promise { - const failed: string[] = []; - const installedExtensionsManifests: IExtensionManifest[] = []; - if (extensions.length) { - console.log(localize('installingExtensions', "Installing extensions...")); - } + // Services + const [instantiationService, appenders] = await this.initServices(); - const installed = await this.extensionManagementService.getInstalled(ExtensionType.User); - const checkIfNotInstalled = (id: string, version?: string): boolean => { - const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id })); - if (installedExtension) { - if (!version && !force) { - console.log(localize('alreadyInstalled-checkAndUpdate', "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@' to install a specific version, for example: '{2}@1.2.3'.", id, installedExtension.manifest.version, id)); - return false; - } - if (version && installedExtension.manifest.version === version) { - console.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", `${id}@${version}`)); - return false; - } - } - return true; - }; - const vsixs: string[] = []; - const installExtensionInfos: InstallExtensionInfo[] = []; - for (const extension of extensions) { - if (/\.vsix$/i.test(extension)) { - vsixs.push(extension); - } else { - const [id, version] = getIdAndVersion(extension); - if (checkIfNotInstalled(id, version)) { - installExtensionInfos.push({ id, version, installOptions: { isBuiltin: false, isMachineScoped } }); - } - } - } - for (const extension of builtinExtensionIds) { - const [id, version] = getIdAndVersion(extension); - if (checkIfNotInstalled(id, version)) { - installExtensionInfos.push({ id, version, installOptions: { isBuiltin: true, isMachineScoped: false } }); - } - } + return instantiationService.invokeFunction(async accessor => { + const logService = accessor.get(ILogService); + const environmentService = accessor.get(INativeEnvironmentService); + const extensionManagementCLIService = accessor.get(IExtensionManagementCLIService); - if (vsixs.length) { - await Promise.all(vsixs.map(async vsix => { - try { - const manifest = await this.installVSIX(vsix, force); - if (manifest) { - installedExtensionsManifests.push(manifest); - } - } catch (err) { - console.error(err.message || err.stack || err); - failed.push(vsix); - } - })); - } + // Log info + logService.info('CLI main', this.argv); - if (installExtensionInfos.length) { + // Error handler + this.registerErrorHandler(logService); - const galleryExtensions = await this.getGalleryExtensions(installExtensionInfos); + // Run based on argv + await this.doRun(environmentService, extensionManagementCLIService); - await Promise.all(installExtensionInfos.map(async extensionInfo => { - const gallery = galleryExtensions.get(extensionInfo.id.toLowerCase()); - if (gallery) { - try { - const manifest = await this.installFromGallery(extensionInfo, gallery, installed, force); - if (manifest) { - installedExtensionsManifests.push(manifest); - } - } catch (err) { - console.error(err.message || err.stack || err); - failed.push(extensionInfo.id); - } - } else { - console.error(`${notFound(extensionInfo.version ? `${extensionInfo.id}@${extensionInfo.version}` : extensionInfo.id)}\n${useId}`); - failed.push(extensionInfo.id); - } - })); - - } - - if (installedExtensionsManifests.some(manifest => isLanguagePackExtension(manifest))) { - await this.updateLocalizationsCache(); - } - - if (failed.length) { - throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', '))); - } - } - - private async installVSIX(vsix: string, force: boolean): Promise { - vsix = path.isAbsolute(vsix) ? vsix : path.join(process.cwd(), vsix); - const manifest = await getManifest(vsix); - const valid = await this.validate(manifest, force); - if (valid) { - try { - await this.extensionManagementService.install(URI.file(vsix)); - console.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(vsix))); - return manifest; - } catch (error) { - if (isPromiseCanceledError(error)) { - console.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", getBaseLabel(vsix))); - return null; - } else { - throw error; - } - } - } - return null; - } - - private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise> { - const extensionIds = extensions.filter(({ version }) => version === undefined).map(({ id }) => id); - const extensionsWithIdAndVersion = extensions.filter(({ version }) => version !== undefined); - - const galleryExtensions = new Map(); - await Promise.all([ - (async () => { - const result = await this.extensionGalleryService.getExtensions(extensionIds, CancellationToken.None); - result.forEach(extension => galleryExtensions.set(extension.identifier.id.toLowerCase(), extension)); - })(), - Promise.all(extensionsWithIdAndVersion.map(async ({ id, version }) => { - const extension = await this.extensionGalleryService.getCompatibleExtension({ id }, version); - if (extension) { - galleryExtensions.set(extension.identifier.id.toLowerCase(), extension); - } - })) - ]); - - return galleryExtensions; - } - - private async installFromGallery({ id, version, installOptions }: InstallExtensionInfo, galleryExtension: IGalleryExtension, installed: ILocalExtension[], force: boolean): Promise { - const manifest = await this.extensionGalleryService.getManifest(galleryExtension, CancellationToken.None); - const installedExtension = installed.find(e => areSameExtensions(e.identifier, galleryExtension.identifier)); - if (installedExtension) { - if (galleryExtension.version === installedExtension.manifest.version) { - console.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", version ? `${id}@${version}` : id)); - return null; - } - console.log(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, galleryExtension.version)); - } - - try { - if (installOptions.isBuiltin) { - console.log(localize('installing builtin ', "Installing builtin extension '{0}' v{1}...", id, galleryExtension.version)); - } else { - console.log(localize('installing', "Installing extension '{0}' v{1}...", id, galleryExtension.version)); - } - await this.extensionManagementService.installFromGallery(galleryExtension, installOptions); - console.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, galleryExtension.version)); - return manifest; - } catch (error) { - if (isPromiseCanceledError(error)) { - console.log(localize('cancelInstall', "Cancelled installing extension '{0}'.", id)); - return null; - } else { - throw error; - } - } - } - - private async validate(manifest: IExtensionManifest, force: boolean): Promise { - if (!manifest) { - throw new Error('Invalid vsix'); - } - - const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; - const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); - const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && semver.gt(local.manifest.version, manifest.version)); - - if (newer && !force) { - console.log(localize('forceDowngrade', "A newer version of extension '{0}' v{1} is already installed. Use '--force' option to downgrade to older version.", newer.identifier.id, newer.manifest.version, manifest.version)); - return false; - } - - return true; - } - - private async uninstallExtension(extensions: string[], force: boolean): Promise { - async function getExtensionId(extensionDescription: string): Promise { - if (!/\.vsix$/i.test(extensionDescription)) { - return extensionDescription; - } - - const zipPath = path.isAbsolute(extensionDescription) ? extensionDescription : path.join(process.cwd(), extensionDescription); - const manifest = await getManifest(zipPath); - return getId(manifest); - } - - const uninstalledExtensions: ILocalExtension[] = []; - for (const extension of extensions) { - const id = await getExtensionId(extension); - const installed = await this.extensionManagementService.getInstalled(); - const extensionToUninstall = installed.find(e => areSameExtensions(e.identifier, { id })); - if (!extensionToUninstall) { - throw new Error(`${notInstalled(id)}\n${useId}`); - } - if (extensionToUninstall.type === ExtensionType.System) { - console.log(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be installed", id)); - return; - } - if (extensionToUninstall.isBuiltin && !force) { - console.log(localize('forceUninstall', "Extension '{0}' is marked as a Built-in extension by user. Please use '--force' option to uninstall it.", id)); - return; - } - console.log(localize('uninstalling', "Uninstalling {0}...", id)); - await this.extensionManagementService.uninstall(extensionToUninstall); - uninstalledExtensions.push(extensionToUninstall); - console.log(localize('successUninstall', "Extension '{0}' was successfully uninstalled!", id)); - } - - if (uninstalledExtensions.some(e => isLanguagePackExtension(e.manifest))) { - await this.updateLocalizationsCache(); - } - } - - private async locateExtension(extensions: string[]): Promise { - const installed = await this.extensionManagementService.getInstalled(); - extensions.forEach(e => { - installed.forEach(i => { - if (i.identifier.id === e) { - if (i.location.scheme === Schemas.file) { - console.log(i.location.fsPath); - return; - } - } - }); + // Flush the remaining data in AI adapter (with 1s timeout) + return raceTimeout(combinedAppender(...appenders).flush(), 1000); }); } - private async updateLocalizationsCache(): Promise { - const localizationService = this.instantiationService.createInstance(LocalizationsService); - await localizationService.update(); - localizationService.dispose(); - } -} + private async initServices(): Promise<[IInstantiationService, AppInsightsAppender[]]> { + const services = new ServiceCollection(); -const eventPrefix = 'monacoworkbench'; + // Environment + const environmentService = new NativeEnvironmentService(this.argv); + services.set(IEnvironmentService, environmentService); + services.set(INativeEnvironmentService, environmentService); -export async function main(argv: NativeParsedArgs): Promise { - const services = new ServiceCollection(); - const disposables = new DisposableStore(); + // Init folders + await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? mkdirp(path) : undefined)); - const environmentService = new NativeEnvironmentService(argv); - const logLevel = getLogLevel(environmentService); - const loggers: ILogService[] = []; - loggers.push(new SpdLogService('cli', environmentService.logsPath, logLevel)); - if (logLevel === LogLevel.Trace) { - loggers.push(new ConsoleLogService(logLevel)); - } - const logService = new MultiplexLogService(loggers); - process.once('exit', () => logService.dispose()); - logService.info('main', argv); + // Log + const logLevel = getLogLevel(environmentService); + const loggers: ILogService[] = []; + loggers.push(new SpdLogService('cli', environmentService.logsPath, logLevel)); + if (logLevel === LogLevel.Trace) { + loggers.push(new ConsoleLogService(logLevel)); + } - await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath] - .map((path): undefined | Promise => path ? mkdirp(path) : undefined)); + const logService = this._register(new MultiplexLogService(loggers)); + services.set(ILogService, logService); - // Files - const fileService = new FileService(logService); - disposables.add(fileService); - services.set(IFileService, fileService); + // Files + const fileService = this._register(new FileService(logService)); + services.set(IFileService, fileService); - const diskFileSystemProvider = new DiskFileSystemProvider(logService); - disposables.add(diskFileSystemProvider); - fileService.registerProvider(Schemas.file, diskFileSystemProvider); + const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService)); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); - const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); - disposables.add(configurationService); - await configurationService.initialize(); + // Configuration + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); + services.set(IConfigurationService, configurationService); - services.set(IEnvironmentService, environmentService); - services.set(INativeEnvironmentService, environmentService); + // Init config + await configurationService.initialize(); - services.set(ILogService, logService); - services.set(IConfigurationService, configurationService); - services.set(IStateService, new SyncDescriptor(StateService)); - services.set(IProductService, { _serviceBrand: undefined, ...product }); + // State + const stateService = new StateService(environmentService, logService); + services.set(IStateService, stateService); - const instantiationService: IInstantiationService = new InstantiationService(services); - - return instantiationService.invokeFunction(async accessor => { - const stateService = accessor.get(IStateService); + // Product + services.set(IProductService, { _serviceBrand: undefined, ...product }); const { appRoot, extensionsPath, extensionDevelopmentLocationURI, isBuilt, installSourcePath } = environmentService; - const services = new ServiceCollection(); + // Request services.set(IRequestService, new SyncDescriptor(RequestService)); + + // Extensions services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); + services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService)); + // Localizations + services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); + + // Telemetry const appenders: AppInsightsAppender[] = []; if (isBuilt && !extensionDevelopmentLocationURI && !environmentService.disableTelemetry && product.enableTelemetry) { if (product.aiConfig && product.aiConfig.asimovKey) { - appenders.push(new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey)); + appenders.push(new AppInsightsAppender('monacoworkbench', null, product.aiConfig.asimovKey)); } const config: ITelemetryServiceConfig = { appender: combinedAppender(...appenders), sendErrorTelemetry: false, - commonProperties: resolveCommonProperties(product.commit, product.version, stateService.getItem('telemetry.machineId'), product.msftInternalDomains, installSourcePath), + commonProperties: resolveCommonProperties(fileService, release(), process.arch, product.commit, product.version, stateService.getItem('telemetry.machineId'), product.msftInternalDomains, installSourcePath), piiPaths: [appRoot, extensionsPath] }; @@ -434,17 +166,70 @@ export async function main(argv: NativeParsedArgs): Promise { services.set(ITelemetryService, NullTelemetryService); } - const instantiationService2 = instantiationService.createChild(services); - const main = instantiationService2.createInstance(Main); + return [new InstantiationService(services), appenders]; + } - try { - await main.run(argv); + private registerErrorHandler(logService: ILogService): void { - // Flush the remaining data in AI adapter. - // If it does not complete in 1 second, exit the process. - await raceTimeout(combinedAppender(...appenders).flush(), 1000); - } finally { - disposables.dispose(); + // Install handler for unexpected errors + setUnexpectedErrorHandler(error => { + const message = toErrorMessage(error, true); + if (!message) { + return; + } + + logService.error(`[uncaught exception in CLI]: ${message}`); + }); + } + + private async doRun(environmentService: INativeEnvironmentService, extensionManagementCLIService: IExtensionManagementCLIService): Promise { + + // Install Source + if (this.argv['install-source']) { + return this.setInstallSource(environmentService, this.argv['install-source']); } - }); + + // List Extensions + if (this.argv['list-extensions']) { + return extensionManagementCLIService.listExtensions(!!this.argv['show-versions'], this.argv['category']); + } + + // Install Extension + else if (this.argv['install-extension'] || this.argv['install-builtin-extension']) { + return extensionManagementCLIService.installExtensions(this.asExtensionIdOrVSIX(this.argv['install-extension'] || []), this.argv['install-builtin-extension'] || [], !!this.argv['do-not-sync'], !!this.argv['force']); + } + + // Uninstall Extension + else if (this.argv['uninstall-extension']) { + return extensionManagementCLIService.uninstallExtensions(this.asExtensionIdOrVSIX(this.argv['uninstall-extension']), !!this.argv['force']); + } + + // Locate Extension + else if (this.argv['locate-extension']) { + return extensionManagementCLIService.locateExtension(this.argv['locate-extension']); + } + + // Telemetry + else if (this.argv['telemetry']) { + console.log(buildTelemetryMessage(environmentService.appRoot, environmentService.extensionsPath)); + } + } + + private asExtensionIdOrVSIX(inputs: string[]): (string | URI)[] { + return inputs.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(process.cwd(), input)) : input); + } + + private setInstallSource(environmentService: INativeEnvironmentService, installSource: string): Promise { + return writeFile(environmentService.installSourcePath, installSource.slice(0, 30)); + } +} + +export async function main(argv: NativeParsedArgs): Promise { + const cliMain = new CliMain(argv); + + try { + await cliMain.run(); + } finally { + cliMain.dispose(); + } } diff --git a/src/vs/code/node/shellEnv.ts b/src/vs/code/node/shellEnv.ts index 29ef8c143..9454f1d47 100644 --- a/src/vs/code/node/shellEnv.ts +++ b/src/vs/code/node/shellEnv.ts @@ -5,11 +5,12 @@ import { spawn } from 'child_process'; import { generateUuid } from 'vs/base/common/uuid'; -import { isWindows } from 'vs/base/common/platform'; +import { isWindows, platform } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper'; import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { getSystemShell } from 'vs/base/node/shell'; /** * We need to get the environment from a user's shell. @@ -58,7 +59,7 @@ export async function resolveShellEnv(logService: ILogService, args: NativeParse let unixShellEnvPromise: Promise | undefined = undefined; async function doResolveUnixShellEnv(logService: ILogService): Promise { - const promise = new Promise((resolve, reject) => { + const promise = new Promise(async (resolve, reject) => { const runAsNode = process.env['ELECTRON_RUN_AS_NODE']; logService.trace('getUnixShellEnvironment#runAsNode', runAsNode); @@ -78,7 +79,8 @@ async function doResolveUnixShellEnv(logService: ILogService): Promise 2000); return new FontInfo({ zoomLevel: browser.getZoomLevel(), + pixelRatio: browser.getPixelRatio(), fontFamily: bareFontInfo.fontFamily, fontWeight: bareFontInfo.fontWeight, fontSize: bareFontInfo.fontSize, @@ -331,15 +333,15 @@ export class Configuration extends CommonEditorConfiguration { constructor( isSimpleWidget: boolean, - options: IEditorConstructionOptions, + options: Readonly, referenceDomElement: HTMLElement | null = null, private readonly accessibilityService: IAccessibilityService ) { super(isSimpleWidget, options); - this._elementSizeObserver = this._register(new ElementSizeObserver(referenceDomElement, options.dimension, () => this._onReferenceDomElementSizeChanged())); + this._elementSizeObserver = this._register(new ElementSizeObserver(referenceDomElement, options.dimension, () => this._recomputeOptions())); - this._register(CSSBasedConfiguration.INSTANCE.onDidChange(() => this._onCSSBasedConfigurationChanged())); + this._register(CSSBasedConfiguration.INSTANCE.onDidChange(() => this._recomputeOptions())); if (this._validatedOptions.get(EditorOption.automaticLayout)) { this._elementSizeObserver.startObserving(); @@ -351,28 +353,24 @@ export class Configuration extends CommonEditorConfiguration { this._recomputeOptions(); } - private _onReferenceDomElementSizeChanged(): void { - this._recomputeOptions(); - } - - private _onCSSBasedConfigurationChanged(): void { - this._recomputeOptions(); - } - public observeReferenceElement(dimension?: IDimension): void { this._elementSizeObserver.observe(dimension); } - public dispose(): void { - super.dispose(); + public updatePixelRatio(): void { + this._recomputeOptions(); } - private _getExtraEditorClassName(): string { + private static _getExtraEditorClassName(): string { let extra = ''; if (!browser.isSafari && !browser.isWebkitWebView) { // Use user-select: none in all browsers except Safari and native macOS WebView extra += 'no-user-select '; } + if (browser.isSafari) { + // See https://github.com/microsoft/vscode/issues/108822 + extra += 'no-minimap-shadow '; + } if (platform.isMacintosh) { extra += 'mac '; } @@ -381,7 +379,7 @@ export class Configuration extends CommonEditorConfiguration { protected _getEnvConfiguration(): IEnvConfiguration { return { - extraEditorClassName: this._getExtraEditorClassName(), + extraEditorClassName: Configuration._getExtraEditorClassName(), outerWidth: this._elementSizeObserver.getWidth(), outerHeight: this._elementSizeObserver.getHeight(), emptySelectionClipboard: browser.isWebKit || browser.isFirefox, diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 06eecaa6a..31f3a0b68 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -110,7 +110,12 @@ export class MouseHandler extends ViewEventHandler { return; } const e = new StandardWheelEvent(browserEvent); - if (e.browserEvent!.ctrlKey || e.browserEvent!.metaKey) { + const doMouseWheelZoom = ( + platform.isMacintosh + ? (browserEvent.metaKey && !browserEvent.ctrlKey && !browserEvent.shiftKey && !browserEvent.altKey) + : (browserEvent.ctrlKey && !browserEvent.metaKey && !browserEvent.shiftKey && !browserEvent.altKey) + ); + if (doMouseWheelZoom) { const zoomLevel: number = EditorZoom.getZoomLevel(); const delta = e.deltaY > 0 ? 1 : -1; EditorZoom.setZoomLevel(zoomLevel + delta); diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index 37c1d8b05..ada5f46af 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -269,11 +269,11 @@ export class HitTestContext { const viewZoneWhitespace = context.viewLayout.getWhitespaceAtVerticalOffset(mouseVerticalOffset); if (viewZoneWhitespace) { - let viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2, - lineCount = context.model.getLineCount(), - positionBefore: Position | null = null, - position: Position | null, - positionAfter: Position | null = null; + const viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2; + const lineCount = context.model.getLineCount(); + let positionBefore: Position | null = null; + let position: Position | null; + let positionAfter: Position | null = null; if (viewZoneWhitespace.afterLineNumber !== lineCount) { // There are more lines after this view zone @@ -767,7 +767,7 @@ export class MouseTargetFactory { const lineWidth = ctx.getLineWidth(lineNumber); if (request.mouseContentHorizontalOffset > lineWidth) { - if (browser.isEdge && pos.column === 1) { + if (browser.isEdgeLegacy && pos.column === 1) { // See https://github.com/microsoft/vscode/issues/10875 const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineNumber, ctx.model.getLineMaxColumn(lineNumber)), undefined, detail); @@ -940,12 +940,16 @@ export class MouseTargetFactory { } } - // For inline decorations, Gecko returns the `` of the line and the offset is the `` with the inline decoration + // For inline decorations, Gecko sometimes returns the `` of the line and the offset is the `` with the inline decoration + // Some other times, it returns the `` with the inline decoration if (hitResult.offsetNode.nodeType === hitResult.offsetNode.ELEMENT_NODE) { - const parent1 = hitResult.offsetNode.parentNode; // expected to be the view line div + const parent1 = hitResult.offsetNode.parentNode; const parent1ClassName = parent1 && parent1.nodeType === parent1.ELEMENT_NODE ? (parent1).className : null; + const parent2 = parent1 ? parent1.parentNode : null; + const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (parent2).className : null; if (parent1ClassName === ViewLine.CLASS_NAME) { + // it returned the `` of the line and the offset is the `` with the inline decoration const tokenSpan = hitResult.offsetNode.childNodes[Math.min(hitResult.offset, hitResult.offsetNode.childNodes.length - 1)]; if (tokenSpan) { const p = ctx.getPositionFromDOMInfo(tokenSpan, 0); @@ -954,6 +958,13 @@ export class MouseTargetFactory { hitTarget: null }; } + } else if (parent2ClassName === ViewLine.CLASS_NAME) { + // it returned the `` with the inline decoration + const p = ctx.getPositionFromDOMInfo(hitResult.offsetNode, 0); + return { + position: p, + hitTarget: null + }; } } @@ -1014,12 +1025,11 @@ export class MouseTargetFactory { } private static _snapToSoftTabBoundary(position: Position, viewModel: IViewModel): Position { - const minColumn = viewModel.getLineMinColumn(position.lineNumber); const lineContent = viewModel.getLineContent(position.lineNumber); const { tabSize } = viewModel.getTextModelOptions(); - const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - minColumn, tabSize, Direction.Nearest); + const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, Direction.Nearest); if (newPosition !== -1) { - return new Position(position.lineNumber, newPosition + minColumn); + return new Position(position.lineNumber, newPosition + 1); } return position; } diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 2f26618aa..b2f38304e 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -54,7 +54,7 @@ class VisibleTextAreaData { } } -const canUseZeroSizeTextarea = (browser.isEdge || browser.isFirefox); +const canUseZeroSizeTextarea = (browser.isFirefox); export class TextAreaHandler extends ViewPart { @@ -280,14 +280,8 @@ export class TextAreaHandler extends ViewPart { })); this._register(this._textAreaInput.onCompositionUpdate((e: ICompositionData) => { - if (browser.isEdge) { - // Due to isEdgeOrIE (where the textarea was not cleared initially) - // we cannot assume the text consists only of the composited text - this._visibleTextArea = this._visibleTextArea!.setWidth(0); - } else { - // adjust width by its size - this._visibleTextArea = this._visibleTextArea!.setWidth(measureText(e.data, this._fontInfo)); - } + // adjust width by its size + this._visibleTextArea = this._visibleTextArea!.setWidth(measureText(e.data, this._fontInfo)); this._render(); })); diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index bc1e26c1c..60627babe 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -13,7 +13,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import * as strings from 'vs/base/common/strings'; -import { ITextAreaWrapper, ITypeData, TextAreaState } from 'vs/editor/browser/controller/textAreaState'; +import { ITextAreaWrapper, ITypeData, TextAreaState, _debugComposition } from 'vs/editor/browser/controller/textAreaState'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; @@ -147,6 +147,7 @@ export class TextAreaInput extends Disposable { private readonly _host: ITextAreaInputHost; private readonly _textArea: TextAreaWrapper; private readonly _asyncTriggerCut: RunOnceScheduler; + private readonly _asyncFocusGainWriteScreenReaderContent: RunOnceScheduler; private _textAreaState: TextAreaState; private _selectionChangeListener: IDisposable | null; @@ -160,6 +161,7 @@ export class TextAreaInput extends Disposable { this._host = host; this._textArea = this._register(new TextAreaWrapper(textArea)); this._asyncTriggerCut = this._register(new RunOnceScheduler(() => this._onCut.fire(), 0)); + this._asyncFocusGainWriteScreenReaderContent = this._register(new RunOnceScheduler(() => this.writeScreenReaderContent('asyncFocusGain'), 0)); this._textAreaState = TextAreaState.EMPTY; this._selectionChangeListener = null; @@ -193,6 +195,10 @@ export class TextAreaInput extends Disposable { })); this._register(dom.addDisposableListener(textArea.domNode, 'compositionstart', (e: CompositionEvent) => { + if (_debugComposition) { + console.log(`[compositionstart]`, e); + } + if (this._isDoingComposition) { return; } @@ -209,6 +215,9 @@ export class TextAreaInput extends Disposable { ) { // Handling long press case on macOS + arrow key => pretend the character was selected if (lastKeyDown.code === 'ArrowRight' || lastKeyDown.code === 'ArrowLeft') { + if (_debugComposition) { + console.log(`[compositionstart] Handling long press case on macOS + arrow key`, e); + } moveOneCharacterLeft = true; } } @@ -221,8 +230,7 @@ export class TextAreaInput extends Disposable { this._textAreaState.selectionStartPosition ? new Position(this._textAreaState.selectionStartPosition.lineNumber, this._textAreaState.selectionStartPosition.column - 1) : null, this._textAreaState.selectionEndPosition ); - } else if (!browser.isEdge) { - // In IE we cannot set .value when handling 'compositionstart' because the entire composition will get canceled. + } else { this._setAndWriteTextAreaState('compositionstart', TextAreaState.EMPTY); } @@ -251,27 +259,10 @@ export class TextAreaInput extends Disposable { return [newState, typeInput]; }; - const compositionDataInValid = (locale: string): boolean => { - // https://github.com/microsoft/monaco-editor/issues/339 - // Multi-part Japanese compositions reset cursor in Edge/IE, Chinese and Korean IME don't have this issue. - // The reason that we can't use this path for all CJK IME is IE and Edge behave differently when handling Korean IME, - // which breaks this path of code. - if (browser.isEdge && locale === 'ja') { - return true; - } - - return false; - }; - this._register(dom.addDisposableListener(textArea.domNode, 'compositionupdate', (e: CompositionEvent) => { - if (compositionDataInValid(e.locale)) { - const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false); - this._textAreaState = newState; - this._onType.fire(typeInput); - this._onCompositionUpdate.fire(e); - return; + if (_debugComposition) { + console.log(`[compositionupdate]`, e); } - const [newState, typeInput] = deduceComposition(e.data || ''); this._textAreaState = newState; this._onType.fire(typeInput); @@ -279,28 +270,23 @@ export class TextAreaInput extends Disposable { })); this._register(dom.addDisposableListener(textArea.domNode, 'compositionend', (e: CompositionEvent) => { + if (_debugComposition) { + console.log(`[compositionend]`, e); + } // https://github.com/microsoft/monaco-editor/issues/1663 // On iOS 13.2, Chinese system IME randomly trigger an additional compositionend event with empty data if (!this._isDoingComposition) { return; } - if (compositionDataInValid(e.locale)) { - // https://github.com/microsoft/monaco-editor/issues/339 - const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false); - this._textAreaState = newState; - this._onType.fire(typeInput); - } else { - const [newState, typeInput] = deduceComposition(e.data || ''); - this._textAreaState = newState; - this._onType.fire(typeInput); - } - // Due to - // isEdgeOrIE (where the textarea was not cleared initially) - // and isChrome (the textarea is not updated correctly when composition ends) - // and isFirefox (the textare ais not updated correctly after inserting emojis) - // we cannot assume the text at the end consists only of the composited text - if (browser.isEdge || browser.isChrome || browser.isFirefox) { + const [newState, typeInput] = deduceComposition(e.data || ''); + this._textAreaState = newState; + this._onType.fire(typeInput); + + // isChrome: the textarea is not updated correctly when composition ends + // isFirefox: the textarea is not updated correctly after inserting emojis + // => we cannot assume the text at the end consists only of the composited text + if (browser.isChrome || browser.isFirefox) { this._textAreaState = TextAreaState.readFromTextArea(this._textArea); } @@ -375,9 +361,31 @@ export class TextAreaInput extends Disposable { })); this._register(dom.addDisposableListener(textArea.domNode, 'focus', () => { + const hadFocus = this._hasFocus; + this._setHasFocus(true); + + if (browser.isSafari && !hadFocus && this._hasFocus) { + // When "tabbing into" the textarea, immediately after dispatching the 'focus' event, + // Safari will always move the selection at offset 0 in the textarea + this._asyncFocusGainWriteScreenReaderContent.schedule(); + } })); this._register(dom.addDisposableListener(textArea.domNode, 'blur', () => { + if (this._isDoingComposition) { + // See https://github.com/microsoft/vscode/issues/112621 + // where compositionend is not triggered when the editor + // is taken off-dom during a composition + + // Clear the flag to be able to write to the textarea + this._isDoingComposition = false; + + // Clear the textarea to avoid an unwanted cursor type + this.writeScreenReaderContent('blurWithoutCompositionEnd'); + + // Fire artificial composition end + this._onCompositionEnd.fire(); + } this._setHasFocus(false); })); } @@ -513,13 +521,7 @@ export class TextAreaInput extends Disposable { } if (this._hasFocus) { - if (browser.isEdge) { - // Edge has a bug where setting the selection range while the focus event - // is dispatching doesn't work. To reproduce, "tab into" the editor. - this._setAndWriteTextAreaState('focusgain', TextAreaState.EMPTY); - } else { - this.writeScreenReaderContent('focusgain'); - } + this.writeScreenReaderContent('focusgain'); } if (this._hasFocus) { diff --git a/src/vs/editor/browser/controller/textAreaState.ts b/src/vs/editor/browser/controller/textAreaState.ts index 65c815e38..d8a2f2013 100644 --- a/src/vs/editor/browser/controller/textAreaState.ts +++ b/src/vs/editor/browser/controller/textAreaState.ts @@ -8,6 +8,8 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { EndOfLinePreference } from 'vs/editor/common/model'; +export const _debugComposition = false; + export interface ITextAreaWrapper { getValue(): string; setValue(reason: string, value: string): void; @@ -59,7 +61,9 @@ export class TextAreaState { } public writeToTextArea(reason: string, textArea: ITextAreaWrapper, select: boolean): void { - // console.log(Date.now() + ': writeToTextArea ' + reason + ': ' + this.toString()); + if (_debugComposition) { + console.log('writeToTextArea ' + reason + ': ' + this.toString()); + } textArea.setValue(reason, this.value); if (select) { textArea.setSelectionRange(reason, this.selectionStart, this.selectionEnd); @@ -105,9 +109,11 @@ export class TextAreaState { }; } - // console.log('------------------------deduceInput'); - // console.log('PREVIOUS STATE: ' + previousState.toString()); - // console.log('CURRENT STATE: ' + currentState.toString()); + if (_debugComposition) { + console.log('------------------------deduceInput'); + console.log('PREVIOUS STATE: ' + previousState.toString()); + console.log('CURRENT STATE: ' + currentState.toString()); + } let previousValue = previousState.value; let previousSelectionStart = previousState.selectionStart; @@ -133,8 +139,10 @@ export class TextAreaState { currentSelectionEnd -= prefixLength; previousSelectionEnd -= prefixLength; - // console.log('AFTER DIFFING PREVIOUS STATE: <' + previousValue + '>, selectionStart: ' + previousSelectionStart + ', selectionEnd: ' + previousSelectionEnd); - // console.log('AFTER DIFFING CURRENT STATE: <' + currentValue + '>, selectionStart: ' + currentSelectionStart + ', selectionEnd: ' + currentSelectionEnd); + if (_debugComposition) { + console.log('AFTER DIFFING PREVIOUS STATE: <' + previousValue + '>, selectionStart: ' + previousSelectionStart + ', selectionEnd: ' + previousSelectionEnd); + console.log('AFTER DIFFING CURRENT STATE: <' + currentValue + '>, selectionStart: ' + currentSelectionStart + ', selectionEnd: ' + currentSelectionEnd); + } if (couldBeEmojiInput && currentSelectionStart === currentSelectionEnd && previousValue.length > 0) { // on OSX, emojis from the emoji picker are inserted at random locations @@ -196,7 +204,9 @@ export class TextAreaState { // no current selection const replacePreviousCharacters = (previousPrefix.length - prefixLength); - // console.log('REMOVE PREVIOUS: ' + (previousPrefix.length - prefixLength) + ' chars'); + if (_debugComposition) { + console.log('REMOVE PREVIOUS: ' + (previousPrefix.length - prefixLength) + ' chars'); + } return { text: currentValue, diff --git a/src/vs/editor/browser/core/markdownRenderer.ts b/src/vs/editor/browser/core/markdownRenderer.ts index e1eb76c8a..ec24b6b5d 100644 --- a/src/vs/editor/browser/core/markdownRenderer.ts +++ b/src/vs/editor/browser/core/markdownRenderer.ts @@ -88,9 +88,7 @@ export class MarkdownRenderer { const element = document.createElement('span'); - element.innerHTML = MarkdownRenderer._ttpTokenizer - ? MarkdownRenderer._ttpTokenizer.createHTML(value, tokenization) as unknown as string - : tokenizeToString(value, tokenization); + element.innerHTML = (MarkdownRenderer._ttpTokenizer?.createHTML(value, tokenization) ?? tokenizeToString(value, tokenization)) as string; // use "good" font let fontFamily = this._options.codeBlockFontFamily; @@ -105,7 +103,7 @@ export class MarkdownRenderer { }, asyncRenderCallback: () => this._onDidRenderAsync.fire(), actionHandler: { - callback: (content) => this._openerService.open(content, { fromUserGesture: true }).catch(onUnexpectedError), + callback: (content) => this._openerService.open(content, { fromUserGesture: true, allowContributedOpeners: true }).catch(onUnexpectedError), disposeables } }; diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 43305c283..bfa07b8dc 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -361,6 +361,11 @@ export interface IEditorConstructionOptions extends IEditorOptions { } export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { + /** + * The initial editor dimension (to avoid measuring the container). + */ + dimension?: editorCommon.IDimension; + /** * Place overflow widgets inside an external DOM node. * Defaults to an internal DOM node. @@ -1047,7 +1052,7 @@ export interface IDiffEditor extends editorCommon.IEditor { /** *@internal */ -export function isCodeEditor(thing: any): thing is ICodeEditor { +export function isCodeEditor(thing: unknown): thing is ICodeEditor { if (thing && typeof (thing).getEditorType === 'function') { return (thing).getEditorType() === editorCommon.EditorType.ICodeEditor; } else { @@ -1058,7 +1063,7 @@ export function isCodeEditor(thing: any): thing is ICodeEditor { /** *@internal */ -export function isDiffEditor(thing: any): thing is IDiffEditor { +export function isDiffEditor(thing: unknown): thing is IDiffEditor { if (thing && typeof (thing).getEditorType === 'function') { return (thing).getEditorType() === editorCommon.EditorType.IDiffEditor; } else { @@ -1069,8 +1074,8 @@ export function isDiffEditor(thing: any): thing is IDiffEditor { /** *@internal */ -export function isCompositeEditor(thing: any): thing is editorCommon.ICompositeCodeEditor { - return thing +export function isCompositeEditor(thing: unknown): thing is editorCommon.ICompositeCodeEditor { + return !!thing && typeof thing === 'object' && typeof (thing).onDidChangeActiveEditor === 'function'; @@ -1079,7 +1084,7 @@ export function isCompositeEditor(thing: any): thing is editorCommon.ICompositeC /** *@internal */ -export function getCodeEditor(thing: any): ICodeEditor | null { +export function getCodeEditor(thing: unknown): ICodeEditor | null { if (isCodeEditor(thing)) { return thing; } diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index a9e8b99dc..5a49c330f 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -69,11 +69,11 @@ export interface IBulkEditOptions { progress?: IProgress; token?: CancellationToken; showPreview?: boolean; - suppressPreview?: boolean; label?: string; quotableLabel?: string; undoRedoSource?: UndoRedoSource; undoRedoGroupId?: number; + confirmBeforeUndo?: boolean; } export interface IBulkEditResult { diff --git a/src/vs/editor/browser/services/codeEditorServiceImpl.ts b/src/vs/editor/browser/services/codeEditorServiceImpl.ts index c961bfd79..952b317c6 100644 --- a/src/vs/editor/browser/services/codeEditorServiceImpl.ts +++ b/src/vs/editor/browser/services/codeEditorServiceImpl.ts @@ -347,6 +347,8 @@ const _CSS_MAP: { [prop: string]: string; } = { fontStyle: 'font-style:{0};', fontWeight: 'font-weight:{0};', + fontSize: 'font-size:{0};', + fontFamily: 'font-family:{0};', textDecoration: 'text-decoration:{0};', cursor: 'cursor:{0};', letterSpacing: 'letter-spacing:{0};', @@ -357,6 +359,7 @@ const _CSS_MAP: { [prop: string]: string; } = { contentText: 'content:\'{0}\';', contentIconPath: 'content:{0};', margin: 'margin:{0};', + padding: 'padding:{0};', width: 'width:{0};', height: 'height:{0};' }; @@ -529,7 +532,7 @@ class DecorationCSSRules { cssTextArr.push(strings.format(_CSS_MAP.contentText, escaped)); } - this.collectCSSText(opts, ['fontStyle', 'fontWeight', 'textDecoration', 'color', 'opacity', 'backgroundColor', 'margin'], cssTextArr); + this.collectCSSText(opts, ['fontStyle', 'fontWeight', 'fontSize', 'fontFamily', 'textDecoration', 'color', 'opacity', 'backgroundColor', 'margin', 'padding'], cssTextArr); if (this.collectCSSText(opts, ['width', 'height'], cssTextArr)) { cssTextArr.push('display:inline-block;'); } diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 1360871f7..dd329d44f 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -4,18 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; import { LinkedList } from 'vs/base/common/linkedList'; +import { ResourceMap } from 'vs/base/common/map'; import { parse } from 'vs/base/common/marshalling'; import { Schemas } from 'vs/base/common/network'; import { normalizePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IOpener, IOpenerService, IValidator, IExternalUriResolver, OpenOptions, ResolveExternalUriOptions, IResolvedExternalUri, IExternalOpener, matchesScheme } from 'vs/platform/opener/common/opener'; import { EditorOpenContext } from 'vs/platform/editor/common/editor'; -import { ResourceMap } from 'vs/base/common/map'; - +import { IExternalOpener, IExternalUriResolver, IOpener, IOpenerService, IResolvedExternalUri, IValidator, matchesScheme, OpenOptions, ResolveExternalUriOptions } from 'vs/platform/opener/common/opener'; class CommandOpener implements IOpener { @@ -100,15 +100,16 @@ export class OpenerService implements IOpenerService { private readonly _resolvers = new LinkedList(); private readonly _resolvedUriTargets = new ResourceMap(uri => uri.with({ path: null, fragment: null, query: null }).toString()); - private _externalOpener: IExternalOpener; + private _defaultExternalOpener: IExternalOpener; + private readonly _externalOpeners = new LinkedList(); constructor( @ICodeEditorService editorService: ICodeEditorService, @ICommandService commandService: ICommandService, ) { // Default external opener is going through window.open() - this._externalOpener = { - openExternal: href => { + this._defaultExternalOpener = { + openExternal: async href => { // ensure to open HTTP/HTTPS links into new windows // to not trigger a navigation. Any other link is // safe to be set as HREF to prevent a blank window @@ -118,11 +119,11 @@ export class OpenerService implements IOpenerService { } else { window.location.href = href; } - return Promise.resolve(true); + return true; } }; - // Default opener: maito, http(s), command, and catch-all-editors + // Default opener: any external, maito, http(s), command, and catch-all-editors this._openers.push({ open: async (target: URI | string, options?: OpenOptions) => { if (options?.openExternal || matchesScheme(target, Schemas.mailto) || matchesScheme(target, Schemas.http) || matchesScheme(target, Schemas.https)) { @@ -152,8 +153,13 @@ export class OpenerService implements IOpenerService { return { dispose: remove }; } - setExternalOpener(externalOpener: IExternalOpener): void { - this._externalOpener = externalOpener; + setDefaultExternalOpener(externalOpener: IExternalOpener): void { + this._defaultExternalOpener = externalOpener; + } + + registerExternalOpener(opener: IExternalOpener): IDisposable { + const remove = this._externalOpeners.push(opener); + return { dispose: remove }; } async open(target: URI | string, options?: OpenOptions): Promise { @@ -196,13 +202,29 @@ export class OpenerService implements IOpenerService { const uri = typeof resource === 'string' ? URI.parse(resource) : resource; const { resolved } = await this.resolveExternalUri(uri, options); + let href: string; if (typeof resource === 'string' && uri.toString() === resolved.toString()) { // open the url-string AS IS - return this._externalOpener.openExternal(resource); + href = resource; } else { // open URI using the toString(noEncode)+encodeURI-trick - return this._externalOpener.openExternal(encodeURI(resolved.toString(true))); + href = encodeURI(resolved.toString(true)); } + + if (options?.allowContributedOpeners) { + const preferredOpenerId = typeof options?.allowContributedOpeners === 'string' ? options?.allowContributedOpeners : undefined; + for (const opener of this._externalOpeners) { + const didOpen = await opener.openExternal(href, { + sourceUri: uri, + preferredOpenerId, + }, CancellationToken.None); + if (didOpen) { + return true; + } + } + } + + return this._defaultExternalOpener.openExternal(href, { sourceUri: uri }, CancellationToken.None); } dispose() { diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 87c6461d5..cd486fc27 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -111,8 +111,8 @@ function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: numbe allVisibleColumns[i] = tmp[1]; } const html = sb.build(); - const trustedhtml = ttPolicy ? ttPolicy.createHTML(html) : html; - containerDomNode.innerHTML = trustedhtml as unknown as string; + const trustedhtml = ttPolicy?.createHTML(html) ?? html; + containerDomNode.innerHTML = trustedhtml as string; containerDomNode.style.position = 'absolute'; containerDomNode.style.top = '10000'; diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index b656a3f8e..e0620c2a6 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import * as browser from 'vs/base/browser/browser'; import { Selection } from 'vs/editor/common/core/selection'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -65,6 +66,7 @@ export class View extends ViewEventHandler { private readonly _scrollbar: EditorScrollbar; private readonly _context: ViewContext; + private _configPixelRatio: number; private _selections: Selection[]; // The view lines @@ -104,6 +106,7 @@ export class View extends ViewEventHandler { // The view context is passed on to most classes (basically to reduce param. counts in ctors) this._context = new ViewContext(configuration, themeService.getColorTheme(), model); + this._configPixelRatio = this._configPixelRatio = this._context.configuration.options.get(EditorOption.pixelRatio); // Ensure the view is the first event handler in order to update the layout this._context.addEventHandler(this); @@ -298,6 +301,7 @@ export class View extends ViewEventHandler { this._scheduleRender(); } public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { + this._configPixelRatio = this._context.configuration.options.get(EditorOption.pixelRatio); this.domNode.setClassName(this._getEditorClassName()); this._applyLayout(); return false; @@ -330,8 +334,8 @@ export class View extends ViewEventHandler { this._viewLines.dispose(); // Destroy view parts - for (let i = 0, len = this._viewParts.length; i < len; i++) { - this._viewParts[i].dispose(); + for (const viewPart of this._viewParts) { + viewPart.dispose(); } super.dispose(); @@ -354,8 +358,7 @@ export class View extends ViewEventHandler { private _getViewPartsToRender(): ViewPart[] { let result: ViewPart[] = [], resultLen = 0; - for (let i = 0, len = this._viewParts.length; i < len; i++) { - const viewPart = this._viewParts[i]; + for (const viewPart of this._viewParts) { if (viewPart.shouldRender()) { result[resultLen++] = viewPart; } @@ -401,16 +404,20 @@ export class View extends ViewEventHandler { const renderingContext = new RenderingContext(this._context.viewLayout, viewportData, this._viewLines); // Render the rest of the parts - for (let i = 0, len = viewPartsToRender.length; i < len; i++) { - const viewPart = viewPartsToRender[i]; + for (const viewPart of viewPartsToRender) { viewPart.prepareRender(renderingContext); } - for (let i = 0, len = viewPartsToRender.length; i < len; i++) { - const viewPart = viewPartsToRender[i]; + for (const viewPart of viewPartsToRender) { viewPart.render(renderingContext); viewPart.onDidRender(); } + + // Try to detect browser zooming and paint again if necessary + if (Math.abs(browser.getPixelRatio() - this._configPixelRatio) > 0.001) { + // looks like the pixel ratio has changed + this._context.configuration.updatePixelRatio(); + } } // --- BEGIN CodeEditor helpers @@ -462,8 +469,7 @@ export class View extends ViewEventHandler { if (everything) { // Force everything to render... this._viewLines.forceShouldRender(); - for (let i = 0, len = this._viewParts.length; i < len; i++) { - const viewPart = this._viewParts[i]; + for (const viewPart of this._viewParts) { viewPart.forceShouldRender(); } } diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index 89de8468f..0a1a7ddc0 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -506,15 +506,15 @@ class ViewLayerRenderer { ctx.lines.splice(removeIndex, removeCount); } - private _finishRenderingNewLines(ctx: IRendererContext, domNodeIsEmpty: boolean, newLinesHTML: string, wasNew: boolean[]): void { + private _finishRenderingNewLines(ctx: IRendererContext, domNodeIsEmpty: boolean, newLinesHTML: string | TrustedHTML, wasNew: boolean[]): void { if (ViewLayerRenderer._ttPolicy) { - newLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(newLinesHTML) as unknown as string; // explains the ugly casts -> https://github.com/microsoft/vscode/issues/106396#issuecomment-692625393 + newLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(newLinesHTML as string); } const lastChild = this.domNode.lastChild; if (domNodeIsEmpty || !lastChild) { - this.domNode.innerHTML = newLinesHTML; + this.domNode.innerHTML = newLinesHTML as string; // explains the ugly casts -> https://github.com/microsoft/vscode/issues/106396#issuecomment-692625393; } else { - lastChild.insertAdjacentHTML('afterend', newLinesHTML); + lastChild.insertAdjacentHTML('afterend', newLinesHTML as string); } let currChild = this.domNode.lastChild; @@ -527,13 +527,13 @@ class ViewLayerRenderer { } } - private _finishRenderingInvalidLines(ctx: IRendererContext, invalidLinesHTML: string, wasInvalid: boolean[]): void { + private _finishRenderingInvalidLines(ctx: IRendererContext, invalidLinesHTML: string | TrustedHTML, wasInvalid: boolean[]): void { const hugeDomNode = document.createElement('div'); if (ViewLayerRenderer._ttPolicy) { - invalidLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(invalidLinesHTML) as unknown as string; + invalidLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(invalidLinesHTML as string); } - hugeDomNode.innerHTML = invalidLinesHTML; + hugeDomNode.innerHTML = invalidLinesHTML as string; for (let i = 0; i < ctx.linesLength; i++) { const line = ctx.lines[i]; diff --git a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts index 6208c89fb..b3ab2991e 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts +++ b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts @@ -42,8 +42,8 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { this._contentWidth = layoutInfo.contentWidth; this._selectionIsEmpty = true; this._focused = false; - this._cursorLineNumbers = []; - this._selections = []; + this._cursorLineNumbers = [1]; + this._selections = [new Selection(1, 1, 1, 1)]; this._renderData = null; this._context.addEventHandler(this); diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index d80556fb9..96754ec4b 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -44,7 +44,7 @@ const canUseFastRenderedViewLine = (function () { let monospaceAssumptionsAreValid = true; -const alwaysRenderInlineSelection = (browser.isEdge); +const alwaysRenderInlineSelection = (browser.isEdgeLegacy); export class DomReadingContext { diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.css b/src/vs/editor/browser/viewParts/minimap/minimap.css index f3692e7e4..f4663ece0 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.css +++ b/src/vs/editor/browser/viewParts/minimap/minimap.css @@ -25,3 +25,8 @@ left: -6px; width: 6px; } +.monaco-editor.no-minimap-shadow .minimap-shadow-visible { + position: absolute; + left: -1px; + width: 1px; +} diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 7ca33211a..348315fa9 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -862,6 +862,7 @@ export class Minimap extends ViewPart implements IMinimapModel { } } public onTokensColorsChanged(e: viewEvents.ViewTokensColorsChangedEvent): boolean { + this._onOptionsMaybeChanged(); return this._actual.onTokensColorsChanged(); } public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { diff --git a/src/vs/editor/browser/viewParts/selections/selections.ts b/src/vs/editor/browser/viewParts/selections/selections.ts index cdb00570c..81417f0be 100644 --- a/src/vs/editor/browser/viewParts/selections/selections.ts +++ b/src/vs/editor/browser/viewParts/selections/selections.ts @@ -60,7 +60,7 @@ function toStyled(item: LineVisibleRanges): LineVisibleRangesWithStyle { // TODO@Alex: Remove this once IE11 fixes Bug #524217 // The problem in IE11 is that it does some sort of auto-zooming to accomodate for displays with different pixel density. // Unfortunately, this auto-zooming is buggy around dealing with rounded borders -const isIEWithZoomingIssuesNearRoundedBorders = browser.isEdge; +const isIEWithZoomingIssuesNearRoundedBorders = browser.isEdgeLegacy; export class SelectionsOverlay extends DynamicViewOverlay { diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 4c87ce2bb..15cd2f345 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -24,7 +24,7 @@ import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents' import { ConfigurationChangedEvent, EditorLayoutInfo, IEditorOptions, EditorOption, IComputedEditorOptions, FindComputedEditorOptionValueById, filterValidationDecorations } from 'vs/editor/common/config/editorOptions'; import { Cursor } from 'vs/editor/common/controller/cursor'; import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; -import { ICursorPositionChangedEvent, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; +import { CursorChangeReason, ICursorPositionChangedEvent, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; @@ -241,7 +241,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE constructor( domElement: HTMLElement, - options: editorBrowser.IEditorConstructionOptions, + _options: Readonly, codeEditorWidgetOptions: ICodeEditorWidgetOptions, @IInstantiationService instantiationService: IInstantiationService, @ICodeEditorService codeEditorService: ICodeEditorService, @@ -253,10 +253,11 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE ) { super(); - options = options || {}; + const options = { ..._options }; this._domElement = domElement; this._overflowWidgetsDomNode = options.overflowWidgetsDomNode; + delete options.overflowWidgetsDomNode; this._id = (++EDITOR_ID); this._decorationTypeKeysToIds = {}; this._decorationTypeSubtypes = {}; @@ -331,7 +332,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._codeEditorService.addCodeEditor(this); } - protected _createConfiguration(options: editorBrowser.IEditorConstructionOptions, accessibilityService: IAccessibilityService): editorCommon.IConfiguration { + protected _createConfiguration(options: Readonly, accessibilityService: IAccessibilityService): editorCommon.IConfiguration { return new Configuration(this.isSimpleWidget, options, this._domElement, accessibilityService); } @@ -366,7 +367,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return this._instantiationService.invokeFunction(fn); } - public updateOptions(newOptions: IEditorOptions): void { + public updateOptions(newOptions: Readonly): void { this._configuration.updateOptions(newOptions); } @@ -811,7 +812,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE ); } - public setSelections(ranges: readonly ISelection[], source: string = 'api'): void { + public setSelections(ranges: readonly ISelection[], source: string = 'api', reason = CursorChangeReason.NotSet): void { if (!this._modelData) { return; } @@ -823,7 +824,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE throw new Error('Invalid arguments'); } } - this._modelData.viewModel.setSelections(source, ranges); + this._modelData.viewModel.setSelections(source, ranges, reason); } public getContentWidth(): number { @@ -1834,6 +1835,7 @@ export class EditorModeContext extends Disposable { private readonly _hasMultipleDocumentFormattingProvider: IContextKey; private readonly _hasMultipleDocumentSelectionFormattingProvider: IContextKey; private readonly _hasSignatureHelpProvider: IContextKey; + private readonly _hasInlineHintsProvider: IContextKey; private readonly _isInWalkThrough: IContextKey; constructor( @@ -1856,6 +1858,7 @@ export class EditorModeContext extends Disposable { this._hasReferenceProvider = EditorContextKeys.hasReferenceProvider.bindTo(_contextKeyService); this._hasRenameProvider = EditorContextKeys.hasRenameProvider.bindTo(_contextKeyService); this._hasSignatureHelpProvider = EditorContextKeys.hasSignatureHelpProvider.bindTo(_contextKeyService); + this._hasInlineHintsProvider = EditorContextKeys.hasInlineHintsProvider.bindTo(_contextKeyService); this._hasDocumentFormattingProvider = EditorContextKeys.hasDocumentFormattingProvider.bindTo(_contextKeyService); this._hasDocumentSelectionFormattingProvider = EditorContextKeys.hasDocumentSelectionFormattingProvider.bindTo(_contextKeyService); this._hasMultipleDocumentFormattingProvider = EditorContextKeys.hasMultipleDocumentFormattingProvider.bindTo(_contextKeyService); @@ -1884,6 +1887,7 @@ export class EditorModeContext extends Disposable { this._register(modes.DocumentFormattingEditProviderRegistry.onDidChange(update)); this._register(modes.DocumentRangeFormattingEditProviderRegistry.onDidChange(update)); this._register(modes.SignatureHelpProviderRegistry.onDidChange(update)); + this._register(modes.InlineHintsProviderRegistry.onDidChange(update)); update(); } @@ -1935,6 +1939,7 @@ export class EditorModeContext extends Disposable { this._hasReferenceProvider.set(modes.ReferenceProviderRegistry.has(model)); this._hasRenameProvider.set(modes.RenameProviderRegistry.has(model)); this._hasSignatureHelpProvider.set(modes.SignatureHelpProviderRegistry.has(model)); + this._hasInlineHintsProvider.set(modes.InlineHintsProviderRegistry.has(model)); this._hasDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.has(model) || modes.DocumentRangeFormattingEditProviderRegistry.has(model)); this._hasDocumentSelectionFormattingProvider.set(modes.DocumentRangeFormattingEditProviderRegistry.has(model)); this._hasMultipleDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.all(model).length + modes.DocumentRangeFormattingEditProviderRegistry.all(model).length > 1); diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 0730e4f00..53615b318 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -12,15 +12,14 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import * as objects from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import { Configuration } from 'vs/editor/browser/config/configuration'; import { StableEditorScrollState } from 'vs/editor/browser/core/editorState'; import * as editorBrowser from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; import { DiffReview } from 'vs/editor/browser/widget/diffReview'; -import { IDiffEditorOptions, IEditorOptions, EditorLayoutInfo, EditorOption, EditorOptions, EditorFontLigatures, stringSet as validateStringSetOption, boolean as validateBooleanOption } from 'vs/editor/common/config/editorOptions'; +import { IDiffEditorOptions, EditorLayoutInfo, EditorOption, EditorOptions, EditorFontLigatures, stringSet as validateStringSetOption, boolean as validateBooleanOption } from 'vs/editor/common/config/editorOptions'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; @@ -54,6 +53,11 @@ import { IViewLineTokens } from 'vs/editor/common/core/lineTokens'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +export interface IDiffCodeEditorWidgetOptions { + originalEditor?: ICodeEditorWidgetOptions; + modifiedEditor?: ICodeEditorWidgetOptions; +} + interface IEditorDiffDecorations { decorations: IModelDeltaDecoration[]; overviewZones: OverviewRulerZone[]; @@ -109,7 +113,7 @@ class VisualEditorState { this._decorations = editor.deltaDecorations(this._decorations, []); } - public apply(editor: CodeEditorWidget, overviewRuler: editorBrowser.IOverviewRuler, newDecorations: IEditorDiffDecorationsWithZones, restoreScrollState: boolean): void { + public apply(editor: CodeEditorWidget, overviewRuler: editorBrowser.IOverviewRuler | null, newDecorations: IEditorDiffDecorationsWithZones, restoreScrollState: boolean): void { const scrollState = restoreScrollState ? StableEditorScrollState.capture(editor) : null; @@ -212,6 +216,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE private _maxComputationTime: number; private _renderIndicators: boolean; private _enableSplitViewResizing: boolean; + private _renderOverviewRuler: boolean; private _strategy!: DiffEditorWidgetStyle; private readonly _updateDecorationsRunner: RunOnceScheduler; @@ -226,7 +231,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE constructor( domElement: HTMLElement, - options: editorBrowser.IDiffEditorConstructionOptions, + options: Readonly, + codeEditorWidgetOptions: IDiffCodeEditorWidgetOptions, @IClipboardService clipboardService: IClipboardService, @IEditorWorkerService editorWorkerService: IEditorWorkerService, @IContextKeyService contextKeyService: IContextKeyService, @@ -287,6 +293,11 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._contextKeyService.createKey('isInEmbeddedDiffEditor', false); } + this._renderOverviewRuler = true; + if (typeof options.renderOverviewRuler !== 'undefined') { + this._renderOverviewRuler = Boolean(options.renderOverviewRuler); + } + this._updateDecorationsRunner = this._register(new RunOnceScheduler(() => this._updateDecorations(), 0)); this._containerDomElement = document.createElement('div'); @@ -308,7 +319,9 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._register(dom.addStandardDisposableListener(this._overviewDomElement, 'mousedown', (e) => { this._modifiedEditor.delegateVerticalScrollbarMouseDown(e); })); - this._containerDomElement.appendChild(this._overviewDomElement); + if (this._renderOverviewRuler) { + this._containerDomElement.appendChild(this._overviewDomElement); + } // Create left side this._originalDomNode = document.createElement('div'); @@ -334,7 +347,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._isVisible = true; this._isHandlingScrollEvent = false; - this._elementSizeObserver = this._register(new ElementSizeObserver(this._containerDomElement, undefined, () => this._onDidContainerSizeChanged())); + this._elementSizeObserver = this._register(new ElementSizeObserver(this._containerDomElement, options.dimension, () => this._onDidContainerSizeChanged())); if (options.automaticLayout) { this._elementSizeObserver.startObserving(); } @@ -353,8 +366,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE rightServices.set(IContextKeyService, rightContextKeyService); const rightScopedInstantiationService = instantiationService.createChild(rightServices); - this._originalEditor = this._createLeftHandSideEditor(options, leftScopedInstantiationService, leftContextKeyService); - this._modifiedEditor = this._createRightHandSideEditor(options, rightScopedInstantiationService, rightContextKeyService); + this._originalEditor = this._createLeftHandSideEditor(options, codeEditorWidgetOptions.originalEditor || {}, leftScopedInstantiationService, leftContextKeyService); + this._modifiedEditor = this._createRightHandSideEditor(options, codeEditorWidgetOptions.modifiedEditor || {}, rightScopedInstantiationService, rightContextKeyService); this._originalOverviewRuler = null; this._modifiedOverviewRuler = null; @@ -415,6 +428,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return this._modifiedEditor.getContentHeight(); } + public getViewWidth(): number { + return this._elementSizeObserver.getWidth(); + } + private _setState(newState: editorBrowser.DiffEditorState): void { if (this._state === newState) { return; @@ -453,6 +470,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } private _recreateOverviewRulers(): void { + if (!this._renderOverviewRuler) { + return; + } + if (this._originalOverviewRuler) { this._overviewDomElement.removeChild(this._originalOverviewRuler.getDomNode()); this._originalOverviewRuler.dispose(); @@ -474,8 +495,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._layoutOverviewRulers(); } - private _createLeftHandSideEditor(options: editorBrowser.IDiffEditorConstructionOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { - const editor = this._createInnerEditor(instantiationService, this._originalDomNode, this._adjustOptionsForLeftHandSide(options)); + private _createLeftHandSideEditor(options: Readonly, codeEditorWidgetOptions: ICodeEditorWidgetOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { + const editor = this._createInnerEditor(instantiationService, this._originalDomNode, this._adjustOptionsForLeftHandSide(options), codeEditorWidgetOptions); this._register(editor.onDidScrollChange((e) => { if (this._isHandlingScrollEvent) { @@ -536,8 +557,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return editor; } - private _createRightHandSideEditor(options: editorBrowser.IDiffEditorConstructionOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { - const editor = this._createInnerEditor(instantiationService, this._modifiedDomNode, this._adjustOptionsForRightHandSide(options)); + private _createRightHandSideEditor(options: Readonly, codeEditorWidgetOptions: ICodeEditorWidgetOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { + const editor = this._createInnerEditor(instantiationService, this._modifiedDomNode, this._adjustOptionsForRightHandSide(options), codeEditorWidgetOptions); this._register(editor.onDidScrollChange((e) => { if (this._isHandlingScrollEvent) { @@ -604,8 +625,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return editor; } - protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: IEditorOptions): CodeEditorWidget { - return instantiationService.createInstance(CodeEditorWidget, container, options, {}); + protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: Readonly, editorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget { + return instantiationService.createInstance(CodeEditorWidget, container, options, editorWidgetOptions); } public dispose(): void { @@ -627,7 +648,9 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._modifiedOverviewRuler.dispose(); } this._overviewDomElement.removeChild(this._overviewViewportDomElement.domNode); - this._containerDomElement.removeChild(this._overviewDomElement); + if (this._renderOverviewRuler) { + this._containerDomElement.removeChild(this._overviewDomElement); + } this._containerDomElement.removeChild(this._originalDomNode); this._originalEditor.dispose(); @@ -678,7 +701,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return this._modifiedEditor; } - public updateOptions(newOptions: IDiffEditorOptions): void { + public updateOptions(newOptions: Readonly): void { // Handle side by side let renderSideBySideChanged = false; @@ -740,6 +763,16 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE // Update class name this._containerDomElement.className = DiffEditorWidget._getClassName(this._themeService.getColorTheme(), this._renderSideBySide); } + + // renderOverviewRuler + if (typeof newOptions.renderOverviewRuler !== 'undefined' && this._renderOverviewRuler !== newOptions.renderOverviewRuler) { + this._renderOverviewRuler = newOptions.renderOverviewRuler; + if (this._renderOverviewRuler) { + this._containerDomElement.appendChild(this._overviewDomElement); + } else { + this._containerDomElement.removeChild(this._overviewDomElement); + } + } } public getModel(): editorCommon.IDiffEditorModel { @@ -749,7 +782,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE }; } - public setModel(model: editorCommon.IDiffEditorModel): void { + public setModel(model: editorCommon.IDiffEditorModel | null): void { // Guard us against partial null model if (model && (!model.original || !model.modified)) { throw new Error(!model.original ? 'DiffEditorWidget.setModel: Original model is null' : 'DiffEditorWidget.setModel: Modified model is null'); @@ -911,7 +944,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } public restoreViewState(s: editorCommon.IDiffEditorViewState): void { - if (s.original && s.modified) { + if (s && s.original && s.modified) { const diffEditorState = s; this._originalEditor.restoreViewState(diffEditorState.original); this._modifiedEditor.restoreViewState(diffEditorState.modified); @@ -969,6 +1002,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } private _layoutOverviewRulers(): void { + if (!this._renderOverviewRuler) { + return; + } + if (!this._originalOverviewRuler || !this._modifiedOverviewRuler) { return; } @@ -1079,9 +1116,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } private _updateDecorations(): void { - if (!this._originalEditor.getModel() || !this._modifiedEditor.getModel() || !this._originalOverviewRuler || !this._modifiedOverviewRuler) { + if (!this._originalEditor.getModel() || !this._modifiedEditor.getModel()) { return; } + const lineChanges = (this._diffComputationResult ? this._diffComputationResult.changes : []); const foreignOriginal = this._originalEditorState.getForeignViewZones(this._originalEditor.getWhitespaces()); @@ -1098,25 +1136,24 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } } - private _adjustOptionsForSubEditor(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IDiffEditorConstructionOptions { - const clonedOptions: editorBrowser.IDiffEditorConstructionOptions = objects.deepClone(options || {}); + private _adjustOptionsForSubEditor(options: Readonly): editorBrowser.IEditorConstructionOptions { + const clonedOptions = { ...options }; clonedOptions.inDiffEditor = true; clonedOptions.automaticLayout = false; - clonedOptions.scrollbar = clonedOptions.scrollbar || {}; + // Clone scrollbar options before changing them + clonedOptions.scrollbar = { ...(clonedOptions.scrollbar || {}) }; clonedOptions.scrollbar.vertical = 'visible'; clonedOptions.folding = false; clonedOptions.codeLens = this._diffCodeLens; clonedOptions.fixedOverflowWidgets = true; - clonedOptions.overflowWidgetsDomNode = options.overflowWidgetsDomNode; // clonedOptions.lineDecorationsWidth = '2ch'; - if (!clonedOptions.minimap) { - clonedOptions.minimap = {}; - } + // Clone minimap options before changing them + clonedOptions.minimap = { ...(clonedOptions.minimap || {}) }; clonedOptions.minimap.enabled = false; return clonedOptions; } - private _adjustOptionsForLeftHandSide(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IEditorConstructionOptions { + private _adjustOptionsForLeftHandSide(options: Readonly): editorBrowser.IEditorConstructionOptions { const result = this._adjustOptionsForSubEditor(options); if (!this._renderSideBySide) { // never wrap hidden editor @@ -1126,16 +1163,28 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } result.readOnly = !this._originalIsEditable; result.extraEditorClassName = 'original-in-monaco-diff-editor'; - return result; + return { + ...result, + dimension: { + height: 0, + width: 0 + } + }; } - private _adjustOptionsForRightHandSide(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IEditorConstructionOptions { + private _adjustOptionsForRightHandSide(options: Readonly): editorBrowser.IEditorConstructionOptions { const result = this._adjustOptionsForSubEditor(options); result.wordWrapOverride1 = this._diffWordWrap; result.revealHorizontalRightPadding = EditorOptions.revealHorizontalRightPadding.defaultValue + DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH; result.scrollbar!.verticalHasArrows = false; result.extraEditorClassName = 'modified-in-monaco-diff-editor'; - return result; + return { + ...result, + dimension: { + height: 0, + width: 0 + } + }; } public doLayout(): void { @@ -1164,7 +1213,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._overviewViewportDomElement.setHeight(30); this._originalEditor.layout({ width: splitPoint, height: (height - reviewHeight) }); - this._modifiedEditor.layout({ width: width - splitPoint - DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH, height: (height - reviewHeight) }); + this._modifiedEditor.layout({ width: width - splitPoint - (this._renderOverviewRuler ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0), height: (height - reviewHeight) }); if (this._originalOverviewRuler || this._modifiedOverviewRuler) { this._layoutOverviewRulers(); @@ -1218,6 +1267,12 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return (this._elementSizeObserver.getHeight() - this._getReviewHeight()); }, + getOptions: () => { + return { + renderOverviewRuler: this._renderOverviewRuler + }; + }, + getContainerDomNode: () => { return this._containerDomElement; }, @@ -1347,6 +1402,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE interface IDataSource { getWidth(): number; getHeight(): number; + getOptions(): { renderOverviewRuler: boolean; }; getContainerDomNode(): HTMLElement; relayoutEditors(): void; @@ -1806,7 +1862,7 @@ class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IVerti public layout(sashRatio: number | null = this._sashRatio): number { const w = this._dataSource.getWidth(); - const contentWidth = w - DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH; + const contentWidth = w - (this._dataSource.getOptions().renderOverviewRuler ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0); let sashPosition = Math.floor((sashRatio || 0.5) * contentWidth); const midPoint = Math.floor(0.5 * contentWidth); @@ -1839,7 +1895,7 @@ class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IVerti private _onSashDrag(e: ISashEvent): void { const w = this._dataSource.getWidth(); - const contentWidth = w - DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH; + const contentWidth = w - (this._dataSource.getOptions().renderOverviewRuler ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0); const sashPosition = this.layout((this._startSashPosition! + (e.currentX - e.startX)) / contentWidth); this._sashRatio = sashPosition / contentWidth; @@ -2370,7 +2426,7 @@ class InlineViewZonesComputer extends ViewZonesComputer { const html = sb.build(); const trustedhtml = ttPolicy ? ttPolicy.createHTML(html) : html; - domNode.innerHTML = trustedhtml as unknown as string; + domNode.innerHTML = trustedhtml as string; viewZone.minWidthInPx = (maxCharsPerLine * typicalHalfwidthCharacterWidth); if (viewLineCounts) { diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index fd53314e1..2fb0eaed0 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -80,6 +80,8 @@ const diffReviewCloseIcon = registerIcon('diff-review-close', Codicon.close, nls export class DiffReview extends Disposable { + private static _ttPolicy = window.trustedTypes?.createPolicy('diffReview', { createHTML: value => value }); + private readonly _diffEditor: DiffEditorWidget; private _isVisible: boolean; public readonly shadow: FastDomNode; @@ -734,14 +736,18 @@ export class DiffReview extends Disposable { let lineContent: string; if (modifiedLine !== 0) { - cell.insertAdjacentHTML('beforeend', - this._renderLine(modifiedModel, modifiedOptions, modifiedModelOpts.tabSize, modifiedLine) - ); + let html: string | TrustedHTML = this._renderLine(modifiedModel, modifiedOptions, modifiedModelOpts.tabSize, modifiedLine); + if (DiffReview._ttPolicy) { + html = DiffReview._ttPolicy.createHTML(html as string); + } + cell.insertAdjacentHTML('beforeend', html as string); lineContent = modifiedModel.getLineContent(modifiedLine); } else { - cell.insertAdjacentHTML('beforeend', - this._renderLine(originalModel, originalOptions, originalModelOpts.tabSize, originalLine) - ); + let html: string | TrustedHTML = this._renderLine(originalModel, originalOptions, originalModelOpts.tabSize, originalLine); + if (DiffReview._ttPolicy) { + html = DiffReview._ttPolicy.createHTML(html as string); + } + cell.insertAdjacentHTML('beforeend', html as string); lineContent = originalModel.getLineContent(originalLine); } diff --git a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts index 5dd98c444..3836a4ec0 100644 --- a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts +++ b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts @@ -82,7 +82,7 @@ export class EmbeddedDiffEditorWidget extends DiffEditorWidget { @IClipboardService clipboardService: IClipboardService, @IEditorProgressService editorProgressService: IEditorProgressService, ) { - super(domElement, parentEditor.getRawOptions(), clipboardService, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService); + super(domElement, parentEditor.getRawOptions(), {}, clipboardService, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService); this._parentEditor = parentEditor; this._overwriteOptions = options; diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index bf1d608fc..c3805bf44 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -272,7 +272,7 @@ function migrateOptions(options: IEditorOptions): void { } } -function deepCloneAndMigrateOptions(_options: IEditorOptions): IEditorOptions { +function deepCloneAndMigrateOptions(_options: Readonly): IEditorOptions { const options = objects.deepClone(_options); migrateOptions(options); return options; @@ -298,7 +298,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC private _readOptions: RawEditorOptions; protected _validatedOptions: ValidatedEditorOptions; - constructor(isSimpleWidget: boolean, _options: IEditorOptions) { + constructor(isSimpleWidget: boolean, _options: Readonly) { super(); this.isSimpleWidget = isSimpleWidget; @@ -318,8 +318,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC public observeReferenceElement(dimension?: IDimension): void { } - public dispose(): void { - super.dispose(); + public updatePixelRatio(): void { } protected _recomputeOptions(): void { @@ -348,7 +347,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC private _computeInternalOptions(): ComputedEditorOptions { const partialEnv = this._getEnvConfiguration(); - const bareFontInfo = BareFontInfo.createFromValidatedSettings(this._validatedOptions, partialEnv.zoomLevel, this.isSimpleWidget); + const bareFontInfo = BareFontInfo.createFromValidatedSettings(this._validatedOptions, partialEnv.zoomLevel, partialEnv.pixelRatio, this.isSimpleWidget); const env: IEnvironmentalOptions = { memory: this._computeOptionsMemory, outerWidth: partialEnv.outerWidth, @@ -394,7 +393,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC return true; } - public updateOptions(_newOptions: IEditorOptions): void { + public updateOptions(_newOptions: Readonly): void { if (typeof _newOptions === 'undefined') { return; } diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 1c6b481c7..bce855361 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -625,6 +625,10 @@ export interface IEditorOptions { * Controls strikethrough deprecated variables. */ showDeprecated?: boolean; + /** + * Control the behavior and rendering of the inline hints. + */ + inlineHints?: IEditorInlineHintsOptions; } /** @@ -677,6 +681,11 @@ export interface IDiffEditorOptions extends IEditorOptions { * Defaults to false */ isInEmbeddedEditor?: boolean; + /** + * Is the diff editor should render overview ruler + * Defaults to true + */ + renderOverviewRuler?: boolean; /** * Control the wrapping of the diff editor. */ @@ -2364,6 +2373,74 @@ class EditorLightbulb extends BaseEditorOption>; + +class EditorInlineHints extends BaseEditorOption { + + constructor() { + const defaults: EditorInlineHintsOptions = { enabled: true, fontSize: 0, fontFamily: EDITOR_FONT_DEFAULTS.fontFamily }; + super( + EditorOption.inlineHints, 'inlineHints', defaults, + { + 'editor.inlineHints.enabled': { + type: 'boolean', + default: defaults.enabled, + description: nls.localize('inlineHints.enable', "Enables the inline hints in the editor.") + }, + 'editor.inlineHints.fontSize': { + type: 'number', + default: defaults.fontSize, + description: nls.localize('inlineHints.fontSize', "Controls font size of inline hints in the editor. When set to `0`, the 90% of `#editor.fontSize#` is used.") + }, + 'editor.inlineHints.fontFamily': { + type: 'string', + default: defaults.fontFamily, + description: nls.localize('inlineHints.fontFamily', "Controls font family of inline hints in the editor.") + }, + } + ); + } + + public validate(_input: any): EditorInlineHintsOptions { + if (!_input || typeof _input !== 'object') { + return this.defaultValue; + } + const input = _input as IEditorInlineHintsOptions; + return { + enabled: boolean(input.enabled, this.defaultValue.enabled), + fontSize: EditorIntOption.clampedInt(input.fontSize, this.defaultValue.fontSize, 0, 100), + fontFamily: EditorStringOption.string(input.fontFamily, this.defaultValue.fontFamily) + }; + } +} + +//#endregion + //#region lineHeight class EditorLineHeight extends EditorIntOption { @@ -3273,7 +3350,7 @@ class EditorSuggest extends BaseEditorOption 0) { this._pushAutoClosedAction(autoClosedCharactersRanges, autoClosedEnclosingRanges); diff --git a/src/vs/editor/common/controller/cursorMoveOperations.ts b/src/vs/editor/common/controller/cursorMoveOperations.ts index 9229c2e35..ac505bdcd 100644 --- a/src/vs/editor/common/controller/cursorMoveOperations.ts +++ b/src/vs/editor/common/controller/cursorMoveOperations.ts @@ -39,11 +39,11 @@ export class MoveOperations { public static leftPositionAtomicSoftTabs(model: ICursorSimpleModel, lineNumber: number, column: number, tabSize: number): Position { const minColumn = model.getLineMinColumn(lineNumber); const lineContent = model.getLineContent(lineNumber); - const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - minColumn, tabSize, Direction.Left); - if (newPosition === -1) { + const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - 1, tabSize, Direction.Left); + if (newPosition === -1 || newPosition + 1 < minColumn) { return this.leftPosition(model, lineNumber, column); } - return new Position(lineNumber, minColumn + newPosition); + return new Position(lineNumber, newPosition + 1); } public static left(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number): CursorPosition { @@ -81,13 +81,12 @@ export class MoveOperations { } public static rightPositionAtomicSoftTabs(model: ICursorSimpleModel, lineNumber: number, column: number, tabSize: number, indentSize: number): Position { - const minColumn = model.getLineMinColumn(lineNumber); const lineContent = model.getLineContent(lineNumber); - const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - minColumn, tabSize, Direction.Right); + const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - 1, tabSize, Direction.Right); if (newPosition === -1) { return this.rightPosition(model, lineNumber, column); } - return new Position(lineNumber, minColumn + newPosition); + return new Position(lineNumber, newPosition + 1); } public static right(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number): CursorPosition { diff --git a/src/vs/editor/common/controller/cursorTypeOperations.ts b/src/vs/editor/common/controller/cursorTypeOperations.ts index 75da04850..0c618026d 100644 --- a/src/vs/editor/common/controller/cursorTypeOperations.ts +++ b/src/vs/editor/common/controller/cursorTypeOperations.ts @@ -351,13 +351,6 @@ export class TypeOperations { if (ir) { let oldEndViewColumn = CursorColumns.visibleColumnFromColumn2(config, model, range.getEndPosition()); const oldEndColumn = range.endColumn; - - let beforeText = '\n'; - if (indentation !== config.normalizeIndentation(ir.beforeEnter)) { - beforeText = config.normalizeIndentation(ir.beforeEnter) + lineText.substring(indentation.length, range.startColumn - 1) + '\n'; - range = new Range(range.startLineNumber, 1, range.endLineNumber, range.endColumn); - } - const newLineContent = model.getLineContent(range.endLineNumber); const firstNonWhitespace = strings.firstNonWhitespaceIndex(newLineContent); if (firstNonWhitespace >= 0) { @@ -367,7 +360,7 @@ export class TypeOperations { } if (keepPosition) { - return new ReplaceCommandWithoutChangingPosition(range, beforeText + config.normalizeIndentation(ir.afterEnter), true); + return new ReplaceCommandWithoutChangingPosition(range, '\n' + config.normalizeIndentation(ir.afterEnter), true); } else { let offset = 0; if (oldEndColumn <= firstNonWhitespace + 1) { @@ -376,7 +369,7 @@ export class TypeOperations { } offset = Math.min(oldEndViewColumn + 1 - config.normalizeIndentation(ir.afterEnter).length - 1, 0); } - return new ReplaceCommandWithOffsetCursorState(range, beforeText + config.normalizeIndentation(ir.afterEnter), 0, offset, true); + return new ReplaceCommandWithOffsetCursorState(range, '\n' + config.normalizeIndentation(ir.afterEnter), 0, offset, true); } } } diff --git a/src/vs/editor/common/diff/diffComputer.ts b/src/vs/editor/common/diff/diffComputer.ts index ebf854605..cac0acd99 100644 --- a/src/vs/editor/common/diff/diffComputer.ts +++ b/src/vs/editor/common/diff/diffComputer.ts @@ -313,6 +313,13 @@ export class DiffComputer { if (this.original.lines.length === 1 && this.original.lines[0].length === 0) { // empty original => fast path + if (this.modified.lines.length === 1 && this.modified.lines[0].length === 0) { + return { + quitEarly: false, + changes: [] + }; + } + return { quitEarly: false, changes: [{ diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 94c4a3c48..e2372db8d 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -157,9 +157,10 @@ export interface IConfiguration extends IDisposable { setMaxLineNumber(maxLineNumber: number): void; setViewLineCount(viewLineCount: number): void; - updateOptions(newOptions: IEditorOptions): void; + updateOptions(newOptions: Readonly): void; getRawOptions(): IEditorOptions; observeReferenceElement(dimension?: IDimension): void; + updatePixelRatio(): void; setIsDominatedByLongLines(isDominatedByLongLines: boolean): void; } @@ -605,6 +606,7 @@ export interface IThemeDecorationRenderOptions { fontStyle?: string; fontWeight?: string; + fontSize?: string; textDecoration?: string; cursor?: string; color?: string | ThemeColor; @@ -629,13 +631,17 @@ export interface IContentDecorationRenderOptions { border?: string; borderColor?: string | ThemeColor; + borderRadius?: string; fontStyle?: string; fontWeight?: string; + fontSize?: string; + fontFamily?: string; textDecoration?: string; color?: string | ThemeColor; backgroundColor?: string | ThemeColor; margin?: string; + padding?: string; width?: string; height?: string; } diff --git a/src/vs/editor/common/editorContextKeys.ts b/src/vs/editor/common/editorContextKeys.ts index a3a3b3de1..2f24e3118 100644 --- a/src/vs/editor/common/editorContextKeys.ts +++ b/src/vs/editor/common/editorContextKeys.ts @@ -61,6 +61,7 @@ export namespace EditorContextKeys { export const hasReferenceProvider = new RawContextKey('editorHasReferenceProvider', false); export const hasRenameProvider = new RawContextKey('editorHasRenameProvider', false); export const hasSignatureHelpProvider = new RawContextKey('editorHasSignatureHelpProvider', false); + export const hasInlineHintsProvider = new RawContextKey('editorHasInlineHintsProvider', false); // -- mode context keys: formatting export const hasDocumentFormattingProvider = new RawContextKey('editorHasDocumentFormattingProvider', false); diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 8f4d53a84..3f1a239dc 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -600,12 +600,6 @@ export interface ITextModel { */ setValue(newValue: string): void; - /** - * Replace the entire text buffer value contained in this model. - * @internal - */ - setValueFromTextBuffer(newValue: ITextBuffer): void; - /** * Get the text stored in this model. * @param eol The end of line character preference. Defaults to `EndOfLinePreference.TextDefined`. @@ -1276,7 +1270,7 @@ export interface ITextBufferBuilder { * @internal */ export interface ITextBufferFactory { - create(defaultEOL: DefaultEndOfLine): ITextBuffer; + create(defaultEOL: DefaultEndOfLine): { textBuffer: ITextBuffer; disposable: IDisposable; }; getFirstLineText(lengthLimit: number): string; } diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts index d134517ba..b65b87a0c 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CharCode } from 'vs/base/common/charCode'; +import { IDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { DefaultEndOfLine, ITextBuffer, ITextBufferBuilder, ITextBufferFactory } from 'vs/editor/common/model'; import { StringBuffer, createLineStarts, createLineStartsFast } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase'; @@ -38,7 +39,7 @@ export class PieceTreeTextBufferFactory implements ITextBufferFactory { return '\n'; } - public create(defaultEOL: DefaultEndOfLine): ITextBuffer { + public create(defaultEOL: DefaultEndOfLine): { textBuffer: ITextBuffer; disposable: IDisposable; } { const eol = this._getEOL(defaultEOL); let chunks = this._chunks; @@ -54,7 +55,8 @@ export class PieceTreeTextBufferFactory implements ITextBufferFactory { } } - return new PieceTreeTextBuffer(chunks, this._bom, eol, this._containsRTL, this._containsUnusualLineTerminators, this._isBasicASCII, this._normalizeEOL); + const textBuffer = new PieceTreeTextBuffer(chunks, this._bom, eol, this._containsRTL, this._containsUnusualLineTerminators, this._isBasicASCII, this._normalizeEOL); + return { textBuffer: textBuffer, disposable: textBuffer }; } public getFirstLineText(lengthLimit: number): string { diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 35dd46640..aab72a48c 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -37,6 +37,7 @@ import { EditorTheme } from 'vs/editor/common/view/viewContext'; import { IUndoRedoService, ResourceEditStackSnapshot } from 'vs/platform/undoRedo/common/undoRedo'; import { TextChange } from 'vs/editor/common/model/textChange'; import { Constants } from 'vs/base/common/uint'; +import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer'; function createTextBufferBuilder() { return new PieceTreeTextBufferBuilder(); @@ -106,7 +107,7 @@ export function createTextBufferFactoryFromSnapshot(snapshot: model.ITextSnapsho return builder.finish(); } -export function createTextBuffer(value: string | model.ITextBufferFactory, defaultEOL: model.DefaultEndOfLine): model.ITextBuffer { +export function createTextBuffer(value: string | model.ITextBufferFactory, defaultEOL: model.DefaultEndOfLine): { textBuffer: model.ITextBuffer; disposable: IDisposable; } { const factory = (typeof value === 'string' ? createTextBufferFactory(value) : value); return factory.create(defaultEOL); } @@ -268,6 +269,7 @@ export class TextModel extends Disposable implements model.ITextModel { private readonly _undoRedoService: IUndoRedoService; private _attachedEditorCount: number; private _buffer: model.ITextBuffer; + private _bufferDisposable: IDisposable; private _options: model.TextModelResolvedOptions; private _isDisposed: boolean; @@ -328,7 +330,9 @@ export class TextModel extends Disposable implements model.ITextModel { this._undoRedoService = undoRedoService; this._attachedEditorCount = 0; - this._buffer = createTextBuffer(source, creationOptions.defaultEOL); + const { textBuffer, disposable } = createTextBuffer(source, creationOptions.defaultEOL); + this._buffer = textBuffer; + this._bufferDisposable = disposable; this._options = TextModel.resolveOptions(this._buffer, creationOptions); @@ -386,10 +390,13 @@ export class TextModel extends Disposable implements model.ITextModel { this._tokenization.dispose(); this._isDisposed = true; super.dispose(); + this._bufferDisposable.dispose(); this._isDisposing = false; // Manually release reference to previous text buffer to avoid large leaks // in case someone leaks a TextModel reference - this._buffer = createTextBuffer('', this._options.defaultEOL); + const emptyDisposedTextBuffer = new PieceTreeTextBuffer([], '', '\n', false, false, true, true); + emptyDisposedTextBuffer.dispose(); + this._buffer = emptyDisposedTextBuffer; } private _assertNotDisposed(): void { @@ -423,8 +430,8 @@ export class TextModel extends Disposable implements model.ITextModel { return; } - const textBuffer = createTextBuffer(value, this._options.defaultEOL); - this.setValueFromTextBuffer(textBuffer); + const { textBuffer, disposable } = createTextBuffer(value, this._options.defaultEOL); + this._setValueFromTextBuffer(textBuffer, disposable); } private _createContentChanged2(range: Range, rangeOffset: number, rangeLength: number, text: string, isUndoing: boolean, isRedoing: boolean, isFlush: boolean): IModelContentChangedEvent { @@ -443,18 +450,16 @@ export class TextModel extends Disposable implements model.ITextModel { }; } - public setValueFromTextBuffer(textBuffer: model.ITextBuffer): void { + private _setValueFromTextBuffer(textBuffer: model.ITextBuffer, textBufferDisposable: IDisposable): void { this._assertNotDisposed(); - if (textBuffer === null) { - // There's nothing to do - return; - } const oldFullModelRange = this.getFullModelRange(); const oldModelValueLength = this.getValueLengthInRange(oldFullModelRange); const endLineNumber = this.getLineCount(); const endColumn = this.getLineMaxColumn(endLineNumber); this._buffer = textBuffer; + this._bufferDisposable.dispose(); + this._bufferDisposable = textBufferDisposable; this._increaseVersionId(); // Flush all tokens diff --git a/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index e127f50e3..3cf7bc9a2 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/src/vs/editor/common/model/textModelTokens.ts @@ -359,7 +359,7 @@ export class TextModelTokenization extends Disposable { const text = this._textModel.getLineContent(lineIndex + 1); const lineStartState = this._tokenizationStateStore.getBeginState(lineIndex); - const r = safeTokenize(languageIdentifier, this._tokenizationSupport, text, lineStartState!); + const r = safeTokenize(languageIdentifier, this._tokenizationSupport, text, true, lineStartState!); builder.add(lineIndex + 1, r.tokens); this._tokenizationStateStore.setEndState(linesLength, lineIndex, r.endState); lineIndex = this._tokenizationStateStore.invalidLineStartIndex - 1; // -1 because the outer loop increments it @@ -410,13 +410,13 @@ export class TextModelTokenization extends Disposable { const languageIdentifier = this._textModel.getLanguageIdentifier(); let state = initialState; for (let i = fakeLines.length - 1; i >= 0; i--) { - let r = safeTokenize(languageIdentifier, this._tokenizationSupport, fakeLines[i], state); + let r = safeTokenize(languageIdentifier, this._tokenizationSupport, fakeLines[i], false, state); state = r.endState; } for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { let text = this._textModel.getLineContent(lineNumber); - let r = safeTokenize(languageIdentifier, this._tokenizationSupport, text, state); + let r = safeTokenize(languageIdentifier, this._tokenizationSupport, text, true, state); builder.add(lineNumber, r.tokens); this._tokenizationStateStore.setFakeTokens(lineNumber - 1); state = r.endState; @@ -443,12 +443,12 @@ function initializeTokenization(textModel: TextModel): [ITokenizationSupport | n return [tokenizationSupport, initialState]; } -function safeTokenize(languageIdentifier: LanguageIdentifier, tokenizationSupport: ITokenizationSupport | null, text: string, state: IState): TokenizationResult2 { +function safeTokenize(languageIdentifier: LanguageIdentifier, tokenizationSupport: ITokenizationSupport | null, text: string, hasEOL: boolean, state: IState): TokenizationResult2 { let r: TokenizationResult2 | null = null; if (tokenizationSupport) { try { - r = tokenizationSupport.tokenize2(text, state.clone(), 0); + r = tokenizationSupport.tokenize2(text, hasEOL, state.clone(), 0); } catch (e) { onUnexpectedError(e); } diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index fe0d7335d..204a4503e 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -211,9 +211,9 @@ export interface ITokenizationSupport { getInitialState(): IState; // add offsetDelta to each of the returned indices - tokenize(line: string, state: IState, offsetDelta: number): TokenizationResult; + tokenize(line: string, hasEOL: boolean, state: IState, offsetDelta: number): TokenizationResult; - tokenize2(line: string, state: IState, offsetDelta: number): TokenizationResult2; + tokenize2(line: string, hasEOL: boolean, state: IState, offsetDelta: number): TokenizationResult2; } /** @@ -1659,6 +1659,19 @@ export interface CodeLensProvider { resolveCodeLens?(model: model.ITextModel, codeLens: CodeLens, token: CancellationToken): ProviderResult; } +export interface InlineHint { + text: string; + range: IRange; + description?: string | IMarkdownString; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; +} + +export interface InlineHintsProvider { + onDidChangeInlineHints?: Event | undefined; + provideInlineHints(model: model.ITextModel, range: Range, token: CancellationToken): ProviderResult; +} + export interface SemanticTokensLegend { readonly tokenTypes: string[]; readonly tokenModifiers: string[]; @@ -1764,6 +1777,11 @@ export const TypeDefinitionProviderRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const InlineHintsProviderRegistry = new LanguageFeatureRegistry(); + /** * @internal */ @@ -1876,3 +1894,14 @@ export interface ITokenizationRegistry { * @internal */ export const TokenizationRegistry = new TokenizationRegistryImpl(); + + +/** + * @internal + */ +export enum ExternalUriOpenerPriority { + None = 0, + Option = 1, + Default = 2, + Preferred = 3, +} diff --git a/src/vs/editor/common/modes/languageConfiguration.ts b/src/vs/editor/common/modes/languageConfiguration.ts index a383c57e9..3542fa74f 100644 --- a/src/vs/editor/common/modes/languageConfiguration.ts +++ b/src/vs/editor/common/modes/languageConfiguration.ts @@ -150,7 +150,7 @@ export interface OnEnterRule { /** * This rule will only execute if the text above the this line matches this regular expression. */ - oneLineAboveText?: RegExp; + previousLineText?: RegExp; /** * The action to execute. */ diff --git a/src/vs/editor/common/modes/languageConfigurationRegistry.ts b/src/vs/editor/common/modes/languageConfigurationRegistry.ts index ccd20e376..6074e34cc 100644 --- a/src/vs/editor/common/modes/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/modes/languageConfigurationRegistry.ts @@ -101,11 +101,11 @@ export class RichEditSupport { return this._electricCharacter; } - public onEnter(autoIndent: EditorAutoIndentStrategy, oneLineAboveText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null { + public onEnter(autoIndent: EditorAutoIndentStrategy, previousLineText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null { if (!this._onEnterSupport) { return null; } - return this._onEnterSupport.onEnter(autoIndent, oneLineAboveText, beforeEnterText, afterEnterText); + return this._onEnterSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText); } private static _mergeConf(prev: LanguageConfiguration | null, current: LanguageConfiguration): LanguageConfiguration { @@ -700,17 +700,17 @@ export class LanguageConfigurationRegistryImpl { afterEnterText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); } - let oneLineAboveText = ''; + let previousLineText = ''; if (range.startLineNumber > 1 && scopedLineTokens.firstCharOffset === 0) { // This is not the first line and the entire line belongs to this mode const oneLineAboveScopedLineTokens = this.getScopedLineTokens(model, range.startLineNumber - 1); if (oneLineAboveScopedLineTokens.languageId === scopedLineTokens.languageId) { // The line above ends with text belonging to the same mode - oneLineAboveText = oneLineAboveScopedLineTokens.getLineContent(); + previousLineText = oneLineAboveScopedLineTokens.getLineContent(); } } - const enterResult = richEditSupport.onEnter(autoIndent, oneLineAboveText, beforeEnterText, afterEnterText); + const enterResult = richEditSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText); if (!enterResult) { return null; } @@ -729,6 +729,8 @@ export class LanguageConfigurationRegistryImpl { } else { appendText = ''; } + } else if (indentAction === IndentAction.Indent) { + appendText = '\t' + appendText; } let indentation = this.getIndentationAtPosition(model, range.startLineNumber, range.startColumn); diff --git a/src/vs/editor/common/modes/modesRegistry.ts b/src/vs/editor/common/modes/modesRegistry.ts index c2ef63388..ffc97c246 100644 --- a/src/vs/editor/common/modes/modesRegistry.ts +++ b/src/vs/editor/common/modes/modesRegistry.ts @@ -58,11 +58,12 @@ export const ModesRegistry = new EditorModesRegistry(); Registry.add(Extensions.ModesRegistry, ModesRegistry); export const PLAINTEXT_MODE_ID = 'plaintext'; +export const PLAINTEXT_EXTENSION = '.txt'; export const PLAINTEXT_LANGUAGE_IDENTIFIER = new LanguageIdentifier(PLAINTEXT_MODE_ID, LanguageId.PlainText); ModesRegistry.registerLanguage({ id: PLAINTEXT_MODE_ID, - extensions: ['.txt'], + extensions: [PLAINTEXT_EXTENSION], aliases: [nls.localize('plainText.alias', "Plain Text"), 'text'], mimetypes: ['text/plain'] }); diff --git a/src/vs/editor/common/modes/supports/onEnter.ts b/src/vs/editor/common/modes/supports/onEnter.ts index f42cc4a4d..c03b52439 100644 --- a/src/vs/editor/common/modes/supports/onEnter.ts +++ b/src/vs/editor/common/modes/supports/onEnter.ts @@ -49,7 +49,7 @@ export class OnEnterSupport { this._regExpRules = opts.onEnterRules || []; } - public onEnter(autoIndent: EditorAutoIndentStrategy, oneLineAboveText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null { + public onEnter(autoIndent: EditorAutoIndentStrategy, previousLineText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null { // (1): `regExpRules` if (autoIndent >= EditorAutoIndentStrategy.Advanced) { for (let i = 0, len = this._regExpRules.length; i < len; i++) { @@ -61,8 +61,8 @@ export class OnEnterSupport { reg: rule.afterText, text: afterEnterText }, { - reg: rule.oneLineAboveText, - text: oneLineAboveText + reg: rule.previousLineText, + text: previousLineText }].every((obj): boolean => { return obj.reg ? obj.reg.test(obj.text) : true; }); diff --git a/src/vs/editor/common/modes/textToHtmlTokenizer.ts b/src/vs/editor/common/modes/textToHtmlTokenizer.ts index c48666416..a0eb66d20 100644 --- a/src/vs/editor/common/modes/textToHtmlTokenizer.ts +++ b/src/vs/editor/common/modes/textToHtmlTokenizer.ts @@ -12,12 +12,12 @@ import { NULL_STATE, nullTokenize2 } from 'vs/editor/common/modes/nullMode'; export interface IReducedTokenizationSupport { getInitialState(): IState; - tokenize2(line: string, state: IState, offsetDelta: number): TokenizationResult2; + tokenize2(line: string, hasEOL: boolean, state: IState, offsetDelta: number): TokenizationResult2; } const fallback: IReducedTokenizationSupport = { getInitialState: () => NULL_STATE, - tokenize2: (buffer: string, state: IState, deltaOffset: number) => nullTokenize2(LanguageId.Null, buffer, state, deltaOffset) + tokenize2: (buffer: string, hasEOL: boolean, state: IState, deltaOffset: number) => nullTokenize2(LanguageId.Null, buffer, state, deltaOffset) }; export function tokenizeToString(text: string, tokenizationSupport: IReducedTokenizationSupport = fallback): string { @@ -110,7 +110,7 @@ function _tokenizeToString(text: string, tokenizationSupport: IReducedTokenizati result += `
    `; } - let tokenizationResult = tokenizationSupport.tokenize2(line, currentState, 0); + let tokenizationResult = tokenizationSupport.tokenize2(line, true, currentState, 0); LineTokens.convertToEndOffset(tokenizationResult.tokens, line.length); let lineTokens = new LineTokens(tokenizationResult.tokens, line); let viewLineTokens = lineTokens.inflate(); diff --git a/src/vs/editor/common/services/getSemanticTokens.ts b/src/vs/editor/common/services/getSemanticTokens.ts new file mode 100644 index 000000000..b0317a83a --- /dev/null +++ b/src/vs/editor/common/services/getSemanticTokens.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; +import { ITextModel } from 'vs/editor/common/model'; +import { DocumentSemanticTokensProviderRegistry, DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits, SemanticTokensLegend, DocumentRangeSemanticTokensProviderRegistry, DocumentRangeSemanticTokensProvider } from 'vs/editor/common/modes'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; +import { assertType } from 'vs/base/common/types'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { encodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; +import { Range } from 'vs/editor/common/core/range'; + +export function isSemanticTokens(v: SemanticTokens | SemanticTokensEdits): v is SemanticTokens { + return v && !!((v).data); +} + +export function isSemanticTokensEdits(v: SemanticTokens | SemanticTokensEdits): v is SemanticTokensEdits { + return v && Array.isArray((v).edits); +} + +export interface IDocumentSemanticTokensResult { + provider: DocumentSemanticTokensProvider; + request: Promise; +} + +export function getDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): IDocumentSemanticTokensResult | null { + const provider = _getDocumentSemanticTokensProvider(model); + if (!provider) { + return null; + } + return { + provider: provider, + request: Promise.resolve(provider.provideDocumentSemanticTokens(model, lastResultId, token)) + }; +} + +function _getDocumentSemanticTokensProvider(model: ITextModel): DocumentSemanticTokensProvider | null { + const result = DocumentSemanticTokensProviderRegistry.ordered(model); + return (result.length > 0 ? result[0] : null); +} + +export function getDocumentRangeSemanticTokensProvider(model: ITextModel): DocumentRangeSemanticTokensProvider | null { + const result = DocumentRangeSemanticTokensProviderRegistry.ordered(model); + return (result.length > 0 ? result[0] : null); +} + +CommandsRegistry.registerCommand('_provideDocumentSemanticTokensLegend', async (accessor, ...args): Promise => { + const [uri] = args; + assertType(uri instanceof URI); + + const model = accessor.get(IModelService).getModel(uri); + if (!model) { + return undefined; + } + + const provider = _getDocumentSemanticTokensProvider(model); + if (!provider) { + // there is no provider => fall back to a document range semantic tokens provider + return accessor.get(ICommandService).executeCommand('_provideDocumentRangeSemanticTokensLegend', uri); + } + + return provider.getLegend(); +}); + +CommandsRegistry.registerCommand('_provideDocumentSemanticTokens', async (accessor, ...args): Promise => { + const [uri] = args; + assertType(uri instanceof URI); + + const model = accessor.get(IModelService).getModel(uri); + if (!model) { + return undefined; + } + + const r = getDocumentSemanticTokens(model, null, CancellationToken.None); + if (!r) { + // there is no provider => fall back to a document range semantic tokens provider + return accessor.get(ICommandService).executeCommand('_provideDocumentRangeSemanticTokens', uri, model.getFullModelRange()); + } + + const { provider, request } = r; + + let result: SemanticTokens | SemanticTokensEdits | null | undefined; + try { + result = await request; + } catch (err) { + onUnexpectedExternalError(err); + return undefined; + } + + if (!result || !isSemanticTokens(result)) { + return undefined; + } + + const buff = encodeSemanticTokensDto({ + id: 0, + type: 'full', + data: result.data + }); + if (result.resultId) { + provider.releaseDocumentSemanticTokens(result.resultId); + } + return buff; +}); + +CommandsRegistry.registerCommand('_provideDocumentRangeSemanticTokensLegend', async (accessor, ...args): Promise => { + const [uri] = args; + assertType(uri instanceof URI); + + const model = accessor.get(IModelService).getModel(uri); + if (!model) { + return undefined; + } + + const provider = getDocumentRangeSemanticTokensProvider(model); + if (!provider) { + return undefined; + } + + return provider.getLegend(); +}); + +CommandsRegistry.registerCommand('_provideDocumentRangeSemanticTokens', async (accessor, ...args): Promise => { + const [uri, range] = args; + assertType(uri instanceof URI); + assertType(Range.isIRange(range)); + + const model = accessor.get(IModelService).getModel(uri); + if (!model) { + return undefined; + } + + const provider = getDocumentRangeSemanticTokensProvider(model); + if (!provider) { + // there is no provider + return undefined; + } + + let result: SemanticTokens | null | undefined; + try { + result = await provider.provideDocumentRangeSemanticTokens(model, Range.lift(range), CancellationToken.None); + } catch (err) { + onUnexpectedExternalError(err); + return undefined; + } + + if (!result || !isSemanticTokens(result)) { + return undefined; + } + + return encodeSemanticTokensDto({ + id: 0, + type: 'full', + data: result.data + }); +}); diff --git a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts index eb2d6795d..a80302ddb 100644 --- a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts +++ b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts @@ -87,13 +87,13 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor this._markerDecorations.clear(); } - getMarker(model: ITextModel, decoration: IModelDecoration): IMarker | null { - const markerDecorations = this._markerDecorations.get(MODEL_ID(model.uri)); + getMarker(uri: URI, decoration: IModelDecoration): IMarker | null { + const markerDecorations = this._markerDecorations.get(MODEL_ID(uri)); return markerDecorations ? (markerDecorations.getMarker(decoration) || null) : null; } - getLiveMarkers(model: ITextModel): [Range, IMarker][] { - const markerDecorations = this._markerDecorations.get(MODEL_ID(model.uri)); + getLiveMarkers(uri: URI): [Range, IMarker][] { + const markerDecorations = this._markerDecorations.get(MODEL_ID(uri)); return markerDecorations ? markerDecorations.getMarkers() : []; } diff --git a/src/vs/editor/common/services/markersDecorationService.ts b/src/vs/editor/common/services/markersDecorationService.ts index 745260c88..221f72379 100644 --- a/src/vs/editor/common/services/markersDecorationService.ts +++ b/src/vs/editor/common/services/markersDecorationService.ts @@ -8,6 +8,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IMarker } from 'vs/platform/markers/common/markers'; import { Event } from 'vs/base/common/event'; import { Range } from 'vs/editor/common/core/range'; +import { URI } from 'vs/base/common/uri'; export const IMarkerDecorationsService = createDecorator('markerDecorationsService'); @@ -16,7 +17,7 @@ export interface IMarkerDecorationsService { onDidChangeMarker: Event; - getMarker(model: ITextModel, decoration: IModelDecoration): IMarker | null; + getMarker(uri: URI, decoration: IModelDecoration): IMarker | null; - getLiveMarkers(model: ITextModel): [Range, IMarker][]; + getLiveMarkers(uri: URI): [Range, IMarker][]; } diff --git a/src/vs/editor/common/services/modeService.ts b/src/vs/editor/common/services/modeService.ts index 2739d6525..e52fbf61a 100644 --- a/src/vs/editor/common/services/modeService.ts +++ b/src/vs/editor/common/services/modeService.ts @@ -50,7 +50,7 @@ export interface IModeService { // --- instantiation create(commaSeparatedMimetypesOrCommaSeparatedIds: string | undefined): ILanguageSelection; createByLanguageName(languageName: string): ILanguageSelection; - createByFilepathOrFirstLine(rsource: URI | null, firstLine?: string): ILanguageSelection; + createByFilepathOrFirstLine(resource: URI | null, firstLine?: string): ILanguageSelection; triggerMode(commaSeparatedMimetypesOrCommaSeparatedIds: string): void; } diff --git a/src/vs/editor/common/services/modeServiceImpl.ts b/src/vs/editor/common/services/modeServiceImpl.ts index 6b2fd6f80..f5a6eba1d 100644 --- a/src/vs/editor/common/services/modeServiceImpl.ts +++ b/src/vs/editor/common/services/modeServiceImpl.ts @@ -40,23 +40,24 @@ class LanguageSelection extends Disposable implements ILanguageSelection { } } -export class ModeServiceImpl implements IModeService { +export class ModeServiceImpl extends Disposable implements IModeService { public _serviceBrand: undefined; private readonly _instantiatedModes: { [modeId: string]: IMode; }; private readonly _registry: LanguagesRegistry; - private readonly _onDidCreateMode = new Emitter(); + private readonly _onDidCreateMode = this._register(new Emitter()); public readonly onDidCreateMode: Event = this._onDidCreateMode.event; - protected readonly _onLanguagesMaybeChanged = new Emitter(); + protected readonly _onLanguagesMaybeChanged = this._register(new Emitter()); public readonly onLanguagesMaybeChanged: Event = this._onLanguagesMaybeChanged.event; constructor(warnOnOverwrite = false) { + super(); this._instantiatedModes = {}; - this._registry = new LanguagesRegistry(true, warnOnOverwrite); - this._registry.onDidChange(() => this._onLanguagesMaybeChanged.fire()); + this._registry = this._register(new LanguagesRegistry(true, warnOnOverwrite)); + this._register(this._registry.onDidChange(() => this._onLanguagesMaybeChanged.fire())); } protected _onReady(): Promise { diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 866727686..15824fd70 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -29,6 +29,7 @@ import { StringSHA1 } from 'vs/base/common/hash'; import { EditStackElement, isEditStackElement } from 'vs/editor/common/model/editStack'; import { Schemas } from 'vs/base/common/network'; import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling'; +import { getDocumentSemanticTokens, isSemanticTokens, isSemanticTokensEdits } from 'vs/editor/common/services/getSemanticTokens'; export interface IEditorSemanticHighlightingOptions { enabled: true | false | 'configuredByTheme'; @@ -412,10 +413,11 @@ export class ModelServiceImpl extends Disposable implements IModelService { public updateModel(model: ITextModel, value: string | ITextBufferFactory): void { const options = this.getCreationOptions(model.getLanguageIdentifier().language, model.uri, model.isForSimpleWidget); - const textBuffer = createTextBuffer(value, options.defaultEOL); + const { textBuffer, disposable } = createTextBuffer(value, options.defaultEOL); // Return early if the text is already set in that form if (model.equalsTextBuffer(textBuffer)) { + disposable.dispose(); return; } @@ -428,6 +430,7 @@ export class ModelServiceImpl extends Disposable implements IModelService { () => [] ); model.pushStackElement(); + disposable.dispose(); } private static _commonPrefix(a: ILineSequence, aLen: number, aDelta: number, b: ILineSequence, bLen: number, bDelta: number): number { @@ -513,58 +516,6 @@ export class ModelServiceImpl extends Disposable implements IModelService { if (!modelData) { return; } - const model = modelData.model; - const sharesUndoRedoStack = (this._undoRedoService.getUriComparisonKey(model.uri) !== model.uri.toString()); - let maintainUndoRedoStack = false; - let heapSize = 0; - if (sharesUndoRedoStack || (this._shouldRestoreUndoStack() && schemaShouldMaintainUndoRedoElements(resource))) { - const elements = this._undoRedoService.getElements(resource); - if (elements.past.length > 0 || elements.future.length > 0) { - for (const element of elements.past) { - if (isEditStackElement(element) && element.matchesResource(resource)) { - maintainUndoRedoStack = true; - heapSize += element.heapSize(resource); - element.setModel(resource); // remove reference from text buffer instance - } - } - for (const element of elements.future) { - if (isEditStackElement(element) && element.matchesResource(resource)) { - maintainUndoRedoStack = true; - heapSize += element.heapSize(resource); - element.setModel(resource); // remove reference from text buffer instance - } - } - } - } - - if (!maintainUndoRedoStack) { - if (!sharesUndoRedoStack) { - const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot(); - if (initialUndoRedoSnapshot !== null) { - this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot); - } - } - modelData.model.dispose(); - return; - } - - const maxMemory = ModelServiceImpl.MAX_MEMORY_FOR_CLOSED_FILES_UNDO_STACK; - if (!sharesUndoRedoStack && heapSize > maxMemory) { - // the undo stack for this file would never fit in the configured memory, so don't bother with it. - const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot(); - if (initialUndoRedoSnapshot !== null) { - this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot); - } - modelData.model.dispose(); - return; - } - - this._ensureDisposedModelsHeapSize(maxMemory - heapSize); - - // We only invalidate the elements, but they remain in the undo-redo service. - this._undoRedoService.setElementsValidFlag(resource, false, (element) => (isEditStackElement(element) && element.matchesResource(resource))); - this._insertDisposedModel(new DisposedModelInfo(resource, modelData.model.getInitialUndoRedoSnapshot(), Date.now(), sharesUndoRedoStack, heapSize, computeModelSha1(model), model.getVersionId(), model.getAlternativeVersionId())); - modelData.model.dispose(); } @@ -599,6 +550,50 @@ export class ModelServiceImpl extends Disposable implements IModelService { const modelId = MODEL_ID(model.uri); const modelData = this._models[modelId]; + const sharesUndoRedoStack = (this._undoRedoService.getUriComparisonKey(model.uri) !== model.uri.toString()); + let maintainUndoRedoStack = false; + let heapSize = 0; + if (sharesUndoRedoStack || (this._shouldRestoreUndoStack() && schemaShouldMaintainUndoRedoElements(model.uri))) { + const elements = this._undoRedoService.getElements(model.uri); + if (elements.past.length > 0 || elements.future.length > 0) { + for (const element of elements.past) { + if (isEditStackElement(element) && element.matchesResource(model.uri)) { + maintainUndoRedoStack = true; + heapSize += element.heapSize(model.uri); + element.setModel(model.uri); // remove reference from text buffer instance + } + } + for (const element of elements.future) { + if (isEditStackElement(element) && element.matchesResource(model.uri)) { + maintainUndoRedoStack = true; + heapSize += element.heapSize(model.uri); + element.setModel(model.uri); // remove reference from text buffer instance + } + } + } + } + + const maxMemory = ModelServiceImpl.MAX_MEMORY_FOR_CLOSED_FILES_UNDO_STACK; + if (!maintainUndoRedoStack) { + if (!sharesUndoRedoStack) { + const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot(); + if (initialUndoRedoSnapshot !== null) { + this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot); + } + } + } else if (!sharesUndoRedoStack && heapSize > maxMemory) { + // the undo stack for this file would never fit in the configured memory, so don't bother with it. + const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot(); + if (initialUndoRedoSnapshot !== null) { + this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot); + } + } else { + this._ensureDisposedModelsHeapSize(maxMemory - heapSize); + // We only invalidate the elements, but they remain in the undo-redo service. + this._undoRedoService.setElementsValidFlag(model.uri, false, (element) => (isEditStackElement(element) && element.matchesResource(model.uri))); + this._insertDisposedModel(new DisposedModelInfo(model.uri, modelData.model.getInitialUndoRedoSnapshot(), Date.now(), sharesUndoRedoStack, heapSize, computeModelSha1(model), model.getVersionId(), model.getAlternativeVersionId())); + } + delete this._models[modelId]; modelData.dispose(); @@ -718,7 +713,9 @@ class SemanticTokensResponse { } } -class ModelSemanticColoring extends Disposable { +export class ModelSemanticColoring extends Disposable { + + public static FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY = 300; private _isDisposed: boolean; private readonly _model: ITextModel; @@ -734,7 +731,7 @@ class ModelSemanticColoring extends Disposable { this._isDisposed = false; this._model = model; this._semanticStyling = stylingProvider; - this._fetchDocumentSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchDocumentSemanticTokensNow(), 300)); + this._fetchDocumentSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchDocumentSemanticTokensNow(), ModelSemanticColoring.FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY)); this._currentDocumentResponse = null; this._currentDocumentRequestCancellationTokenSource = null; this._documentProvidersChangeListeners = []; @@ -788,15 +785,21 @@ class ModelSemanticColoring extends Disposable { // there is already a request running, let it finish... return; } - const provider = this._getSemanticColoringProvider(); - if (!provider) { + + const cancellationTokenSource = new CancellationTokenSource(); + const lastResultId = this._currentDocumentResponse ? this._currentDocumentResponse.resultId || null : null; + const r = getDocumentSemanticTokens(this._model, lastResultId, cancellationTokenSource.token); + if (!r) { + // there is no provider if (this._currentDocumentResponse) { // there are semantic tokens set this._model.setSemanticTokens(null, false); } return; } - this._currentDocumentRequestCancellationTokenSource = new CancellationTokenSource(); + + const { provider, request } = r; + this._currentDocumentRequestCancellationTokenSource = cancellationTokenSource; const pendingChanges: IModelContentChangedEvent[] = []; const contentChangeListener = this._model.onDidChangeContent((e) => { @@ -805,15 +808,13 @@ class ModelSemanticColoring extends Disposable { const styling = this._semanticStyling.get(provider); - const lastResultId = this._currentDocumentResponse ? this._currentDocumentResponse.resultId || null : null; - const request = Promise.resolve(provider.provideDocumentSemanticTokens(this._model, lastResultId, this._currentDocumentRequestCancellationTokenSource.token)); - request.then((res) => { this._currentDocumentRequestCancellationTokenSource = null; contentChangeListener.dispose(); this._setDocumentSemanticTokens(provider, res || null, styling, pendingChanges); }, (err) => { - if (!err || typeof err.message !== 'string' || err.message.indexOf('busy') === -1) { + const isExpectedError = err && (errors.isPromiseCanceledError(err) || (typeof err.message === 'string' && err.message.indexOf('busy') !== -1)); + if (!isExpectedError) { errors.onUnexpectedError(err); } @@ -831,14 +832,6 @@ class ModelSemanticColoring extends Disposable { }); } - private static _isSemanticTokens(v: SemanticTokens | SemanticTokensEdits): v is SemanticTokens { - return v && !!((v).data); - } - - private static _isSemanticTokensEdits(v: SemanticTokens | SemanticTokensEdits): v is SemanticTokensEdits { - return v && Array.isArray((v).edits); - } - private static _copy(src: Uint32Array, srcOffset: number, dest: Uint32Array, destOffset: number, length: number): void { for (let i = 0; i < length; i++) { dest[destOffset + i] = src[srcOffset + i]; @@ -847,6 +840,12 @@ class ModelSemanticColoring extends Disposable { private _setDocumentSemanticTokens(provider: DocumentSemanticTokensProvider | null, tokens: SemanticTokens | SemanticTokensEdits | null, styling: SemanticTokensProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void { const currentResponse = this._currentDocumentResponse; + const rescheduleIfNeeded = () => { + if (pendingChanges.length > 0 && !this._fetchDocumentSemanticTokens.isScheduled()) { + this._fetchDocumentSemanticTokens.schedule(); + } + }; + if (this._currentDocumentResponse) { this._currentDocumentResponse.dispose(); this._currentDocumentResponse = null; @@ -864,10 +863,11 @@ class ModelSemanticColoring extends Disposable { } if (!tokens) { this._model.setSemanticTokens(null, true); + rescheduleIfNeeded(); return; } - if (ModelSemanticColoring._isSemanticTokensEdits(tokens)) { + if (isSemanticTokensEdits(tokens)) { if (!currentResponse) { // not possible! this._model.setSemanticTokens(null, true); @@ -918,7 +918,7 @@ class ModelSemanticColoring extends Disposable { } } - if (ModelSemanticColoring._isSemanticTokens(tokens)) { + if (isSemanticTokens(tokens)) { this._currentDocumentResponse = new SemanticTokensResponse(provider, tokens.resultId, tokens.data); @@ -937,21 +937,13 @@ class ModelSemanticColoring extends Disposable { } } } - - if (!this._fetchDocumentSemanticTokens.isScheduled()) { - this._fetchDocumentSemanticTokens.schedule(); - } } this._model.setSemanticTokens(result, true); - return; + } else { + this._model.setSemanticTokens(null, true); } - this._model.setSemanticTokens(null, true); - } - - private _getSemanticColoringProvider(): DocumentSemanticTokensProvider | null { - const result = DocumentSemanticTokensProviderRegistry.ordered(this._model); - return (result.length > 0 ? result[0] : null); + rescheduleIfNeeded(); } } diff --git a/src/vs/workbench/api/common/shared/semanticTokensDto.ts b/src/vs/editor/common/services/semanticTokensDto.ts similarity index 100% rename from src/vs/workbench/api/common/shared/semanticTokensDto.ts rename to src/vs/editor/common/services/semanticTokensDto.ts diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 8c772e3c3..ad85fc86c 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -287,11 +287,12 @@ export enum EditorOption { wrappingIndent = 117, wrappingStrategy = 118, showDeprecated = 119, - editorClassName = 120, - pixelRatio = 121, - tabFocusMode = 122, - layoutInfo = 123, - wrappingInfo = 124 + inlineHints = 120, + editorClassName = 121, + pixelRatio = 122, + tabFocusMode = 123, + layoutInfo = 124, + wrappingInfo = 125 } /** diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index 24059d23a..9d5583ee0 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -255,7 +255,7 @@ export class ViewLayout extends Disposable implements IViewLayout { let result = this._linesLayout.getLinesTotalHeight(); if (options.get(EditorOption.scrollBeyondLastLine)) { - result += height - options.get(EditorOption.lineHeight); + result += Math.max(0, height - options.get(EditorOption.lineHeight) - options.get(EditorOption.padding).bottom); } else { result += this._getHorizontalScrollbarHeight(width, contentWidth); } diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 67a3b90fa..9640d0076 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -303,6 +303,19 @@ function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterCla breakOffsetVisibleColumn = forcedBreakOffsetVisibleColumn; } + if (breakOffset <= lastBreakingOffset) { + // Make sure that we are advancing (at least one character) + const charCode = lineText.charCodeAt(lastBreakingOffset); + if (strings.isHighSurrogate(charCode)) { + // A surrogate pair must always be considered as a single unit, so it is never to be broken + breakOffset = lastBreakingOffset + 2; + breakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn + 2; + } else { + breakOffset = lastBreakingOffset + 1; + breakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn + computeCharWidth(charCode, lastBreakingOffsetVisibleColumn, tabSize, columnsForFullWidthChar); + } + } + lastBreakingOffset = breakOffset; breakingOffsets[breakingOffsetsCount] = breakOffset; lastBreakingOffsetVisibleColumn = breakOffsetVisibleColumn; @@ -435,6 +448,10 @@ function computeCharWidth(charCode: number, visibleColumn: number, tabSize: numb if (strings.isFullWidthCharacter(charCode)) { return columnsForFullWidthChar; } + if (charCode < 32) { + // when using `editor.renderControlCharacters`, the substitutions are often wide + return columnsForFullWidthChar; + } return 1; } diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 50765f8ee..89ca90006 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -503,15 +503,8 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return null; } - let hiddenAreas = this.getHiddenAreas(); - let isInHiddenArea = false; - let testPosition = new Position(fromLineNumber, 1); - for (const hiddenArea of hiddenAreas) { - if (hiddenArea.containsPosition(testPosition)) { - isInHiddenArea = true; - break; - } - } + // cannot use this.getHiddenAreas() because those decorations have already seen the effect of this model change + const isInHiddenArea = (fromLineNumber > 2 && !this.lines[fromLineNumber - 2].isVisible()); let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(fromLineNumber - 2) + 1); diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 932f6917e..a469a79bb 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -916,8 +916,8 @@ export class ViewModel extends Disposable implements IViewModel { public getPosition(): Position { return this._cursor.getPrimaryCursorState().modelState.position; } - public setSelections(source: string | null | undefined, selections: readonly ISelection[]): void { - this._withViewEventsCollector(eventsCollector => this._cursor.setSelections(eventsCollector, source, selections)); + public setSelections(source: string | null | undefined, selections: readonly ISelection[], reason = CursorChangeReason.NotSet): void { + this._withViewEventsCollector(eventsCollector => this._cursor.setSelections(eventsCollector, source, selections, reason)); } public saveCursorState(): ICursorState[] { return this._cursor.saveState(); diff --git a/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts b/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts index 0a11a1a5f..c59956eb2 100644 --- a/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts +++ b/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts @@ -39,20 +39,20 @@ suite('bracket matching', () => { // start on closing bracket editor.setPosition(new Position(1, 20)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 9)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 19)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 19)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 9)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); // start on opening bracket editor.setPosition(new Position(1, 23)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 31)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 31)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 23)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 31)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 31)); bracketMatchingController.dispose(); }); @@ -71,25 +71,25 @@ suite('bracket matching', () => { // start position between brackets editor.setPosition(new Position(1, 16)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 18)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 18)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 14)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 14)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 18)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 18)); // skip brackets in comments editor.setPosition(new Position(1, 21)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 23)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 24)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 24)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 23)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); // do not break if no brackets are available editor.setPosition(new Position(1, 26)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 26)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 26)); bracketMatchingController.dispose(); }); @@ -109,32 +109,32 @@ suite('bracket matching', () => { // start position in open brackets editor.setPosition(new Position(1, 9)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 20)); - assert.deepEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 20)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); // start position in close brackets editor.setPosition(new Position(1, 20)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 20)); - assert.deepEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 20)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); // start position between brackets editor.setPosition(new Position(1, 16)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 19)); - assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 19)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 19)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 14, 1, 19)); // start position outside brackets editor.setPosition(new Position(1, 21)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 25)); - assert.deepEqual(editor.getSelection(), new Selection(1, 23, 1, 25)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 25)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 23, 1, 25)); // do not break if no brackets are available editor.setPosition(new Position(1, 26)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 26)); - assert.deepEqual(editor.getSelection(), new Selection(1, 26, 1, 26)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 26)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 26, 1, 26)); bracketMatchingController.dispose(); }); @@ -159,7 +159,7 @@ suite('bracket matching', () => { editor.setPosition(new Position(3, 5)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getSelection(), new Selection(5, 1, 5, 1)); + assert.deepStrictEqual(editor.getSelection(), new Selection(5, 1, 5, 1)); bracketMatchingController.dispose(); }); @@ -184,7 +184,7 @@ suite('bracket matching', () => { editor.setPosition(new Position(3, 5)); bracketMatchingController.selectToBracket(false); - assert.deepEqual(editor.getSelection(), new Selection(1, 12, 5, 1)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 12, 5, 1)); bracketMatchingController.dispose(); }); @@ -207,7 +207,7 @@ suite('bracket matching', () => { new Selection(1, 17, 1, 17) ]); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(1, 8, 1, 13), new Selection(1, 16, 1, 19) @@ -220,7 +220,7 @@ suite('bracket matching', () => { new Selection(1, 14, 1, 14) ]); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(1, 8, 1, 13), new Selection(1, 16, 1, 19) @@ -233,7 +233,7 @@ suite('bracket matching', () => { new Selection(1, 19, 1, 19) ]); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(1, 8, 1, 13), new Selection(1, 16, 1, 19) diff --git a/src/vs/editor/contrib/clipboard/clipboard.ts b/src/vs/editor/contrib/clipboard/clipboard.ts index 6860a7bb6..8a0f08ffc 100644 --- a/src/vs/editor/contrib/clipboard/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/clipboard.ts @@ -24,10 +24,11 @@ const CLIPBOARD_CONTEXT_MENU_GROUP = '9_cutcopypaste'; const supportsCut = (platform.isNative || document.queryCommandSupported('cut')); const supportsCopy = (platform.isNative || document.queryCommandSupported('copy')); // IE and Edge have trouble with setting html content in clipboard -const supportsCopyWithSyntaxHighlighting = (supportsCopy && !browser.isEdge); +const supportsCopyWithSyntaxHighlighting = (supportsCopy && !browser.isEdgeLegacy); // Firefox only supports navigator.clipboard.readText() in browser extensions. // See https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText#Browser_compatibility -const supportsPaste = (browser.isFirefox ? document.queryCommandSupported('paste') : true); +// When loading over http, navigator.clipboard can be undefined. See https://github.com/microsoft/monaco-editor/issues/2313 +const supportsPaste = (typeof navigator.clipboard === 'undefined' || browser.isFirefox) ? document.queryCommandSupported('paste') : true; function registerCommand(command: T): T { command.register(); diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 3fddfdff3..da622e6ef 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -98,14 +98,17 @@ export class CodeLensContribution implements IEditorContribution { const fontFamily = this._editor.getOption(EditorOption.codeLensFontFamily); const editorFontInfo = this._editor.getOption(EditorOption.fontInfo); + const fontFamilyVar = `--codelens-font-family${this._styleClassName}`; + let newStyle = ` .monaco-editor .codelens-decoration.${this._styleClassName} { line-height: ${codeLensHeight}px; font-size: ${fontSize}px; padding-right: ${Math.round(fontSize * 0.5)}px; font-feature-settings: ${editorFontInfo.fontFeatureSettings} } .monaco-editor .codelens-decoration.${this._styleClassName} span.codicon { line-height: ${codeLensHeight}px; font-size: ${fontSize}px; } `; if (fontFamily) { - newStyle += `.monaco-editor .codelens-decoration.${this._styleClassName} { font-family: '${fontFamily}'}`; + newStyle += `.monaco-editor .codelens-decoration.${this._styleClassName} { font-family: var(${fontFamilyVar})}`; } this._styleElement.textContent = newStyle; + this._editor.getDomNode()?.style.setProperty(fontFamilyVar, fontFamily ?? 'inherit'); // this._editor.changeViewZones(accessor => { diff --git a/src/vs/editor/contrib/codelens/codelensWidget.ts b/src/vs/editor/contrib/codelens/codelensWidget.ts index f77b2dfc5..9014c904a 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/codelensWidget.ts @@ -14,7 +14,7 @@ import { editorCodeLensForeground } from 'vs/editor/common/view/editorColorRegis import { CodeLensItem } from 'vs/editor/contrib/codelens/codelens'; import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { renderCodicons } from 'vs/base/browser/codicons'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; class CodeLensViewZone implements IViewZone { @@ -88,7 +88,7 @@ class CodeLensContentWidget implements IContentWidget { } hasSymbol = true; if (lens.command) { - const title = renderCodicons(lens.command.title.trim()); + const title = renderLabelWithIcons(lens.command.title.trim()); if (lens.command.id) { children.push(dom.$('a', { id: String(i) }, ...title)); this._commands.set(String(i), lens.command); diff --git a/src/vs/editor/contrib/colorPicker/colorContributions.ts b/src/vs/editor/contrib/colorPicker/colorContributions.ts index 4a997061b..90f91a846 100644 --- a/src/vs/editor/contrib/colorPicker/colorContributions.ts +++ b/src/vs/editor/contrib/colorPicker/colorContributions.ts @@ -47,7 +47,7 @@ export class ColorContribution extends Disposable implements IEditorContribution } const hoverController = this._editor.getContribution(ModesHoverController.ID); - if (!hoverController.contentWidget.isColorPickerVisible()) { + if (!hoverController.isColorPickerVisible()) { const range = new Range(mouseEvent.target.range.startLineNumber, mouseEvent.target.range.startColumn + 1, mouseEvent.target.range.endLineNumber, mouseEvent.target.range.endColumn + 1); hoverController.showContentHover(range, HoverStartMode.Delayed, false); } diff --git a/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts b/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts index 2a4329ba9..81b47f075 100644 --- a/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts +++ b/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts @@ -96,25 +96,25 @@ suite('Editor Contrib - Line Comment Command', () => { throw new Error(`unexpected`); } - assert.equal(r.shouldRemoveComments, false); + assert.strictEqual(r.shouldRemoveComments, false); // Does not change `commentStr` - assert.equal(r.lines[0].commentStr, '//'); - assert.equal(r.lines[1].commentStr, 'rem'); - assert.equal(r.lines[2].commentStr, '!@#'); - assert.equal(r.lines[3].commentStr, '!@#'); + assert.strictEqual(r.lines[0].commentStr, '//'); + assert.strictEqual(r.lines[1].commentStr, 'rem'); + assert.strictEqual(r.lines[2].commentStr, '!@#'); + assert.strictEqual(r.lines[3].commentStr, '!@#'); // Fills in `isWhitespace` - assert.equal(r.lines[0].ignore, true); - assert.equal(r.lines[1].ignore, true); - assert.equal(r.lines[2].ignore, false); - assert.equal(r.lines[3].ignore, false); + assert.strictEqual(r.lines[0].ignore, true); + assert.strictEqual(r.lines[1].ignore, true); + assert.strictEqual(r.lines[2].ignore, false); + assert.strictEqual(r.lines[3].ignore, false); // Fills in `commentStrOffset` - assert.equal(r.lines[0].commentStrOffset, 2); - assert.equal(r.lines[1].commentStrOffset, 4); - assert.equal(r.lines[2].commentStrOffset, 4); - assert.equal(r.lines[3].commentStrOffset, 2); + assert.strictEqual(r.lines[0].commentStrOffset, 2); + assert.strictEqual(r.lines[1].commentStrOffset, 4); + assert.strictEqual(r.lines[2].commentStrOffset, 4); + assert.strictEqual(r.lines[3].commentStrOffset, 2); r = LineCommentCommand._analyzeLines(Type.Toggle, true, createSimpleModel([ @@ -127,31 +127,31 @@ suite('Editor Contrib - Line Comment Command', () => { throw new Error(`unexpected`); } - assert.equal(r.shouldRemoveComments, true); + assert.strictEqual(r.shouldRemoveComments, true); // Does not change `commentStr` - assert.equal(r.lines[0].commentStr, '//'); - assert.equal(r.lines[1].commentStr, 'rem'); - assert.equal(r.lines[2].commentStr, '!@#'); - assert.equal(r.lines[3].commentStr, '!@#'); + assert.strictEqual(r.lines[0].commentStr, '//'); + assert.strictEqual(r.lines[1].commentStr, 'rem'); + assert.strictEqual(r.lines[2].commentStr, '!@#'); + assert.strictEqual(r.lines[3].commentStr, '!@#'); // Fills in `isWhitespace` - assert.equal(r.lines[0].ignore, true); - assert.equal(r.lines[1].ignore, false); - assert.equal(r.lines[2].ignore, false); - assert.equal(r.lines[3].ignore, false); + assert.strictEqual(r.lines[0].ignore, true); + assert.strictEqual(r.lines[1].ignore, false); + assert.strictEqual(r.lines[2].ignore, false); + assert.strictEqual(r.lines[3].ignore, false); // Fills in `commentStrOffset` - assert.equal(r.lines[0].commentStrOffset, 2); - assert.equal(r.lines[1].commentStrOffset, 4); - assert.equal(r.lines[2].commentStrOffset, 4); - assert.equal(r.lines[3].commentStrOffset, 2); + assert.strictEqual(r.lines[0].commentStrOffset, 2); + assert.strictEqual(r.lines[1].commentStrOffset, 4); + assert.strictEqual(r.lines[2].commentStrOffset, 4); + assert.strictEqual(r.lines[3].commentStrOffset, 2); // Fills in `commentStrLength` - assert.equal(r.lines[0].commentStrLength, 2); - assert.equal(r.lines[1].commentStrLength, 4); - assert.equal(r.lines[2].commentStrLength, 4); - assert.equal(r.lines[3].commentStrLength, 3); + assert.strictEqual(r.lines[0].commentStrLength, 2); + assert.strictEqual(r.lines[1].commentStrLength, 4); + assert.strictEqual(r.lines[2].commentStrLength, 4); + assert.strictEqual(r.lines[3].commentStrLength, 3); }); test('_normalizeInsertionPoint', () => { @@ -166,7 +166,7 @@ suite('Editor Contrib - Line Comment Command', () => { }); LineCommentCommand._normalizeInsertionPoint(model, offsets, 1, tabSize); const actual = offsets.map(item => item.commentStrOffset); - assert.deepEqual(actual, expected, testName); + assert.deepStrictEqual(actual, expected, testName); }; // Bug 16696:[comment] comments not aligned in this case @@ -1083,7 +1083,7 @@ suite('Editor Contrib - Line Comment in mixed modes', () => { tokenize: () => { throw new Error('not implemented'); }, - tokenize2: (line: string, state: modes.IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 => { let languageId = (/^ /.test(line) ? INNER_LANGUAGE_ID : OUTER_LANGUAGE_ID); let tokens = new Uint32Array(1 << 1); diff --git a/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts b/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts index 308484142..4a398b498 100644 --- a/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts +++ b/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts @@ -29,16 +29,16 @@ suite('FindController', () => { // press Delete CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, {}); - assert.deepEqual(editor.getValue(), 'hell'); - assert.deepEqual(editor.getSelections(), [new Selection(1, 5, 1, 5)]); + assert.deepStrictEqual(editor.getValue(), 'hell'); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 5, 1, 5)]); // press left CoreNavigationCommands.CursorLeft.runEditorCommand(null, editor, {}); - assert.deepEqual(editor.getSelections(), [new Selection(1, 4, 1, 4)]); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 4, 1, 4)]); // press Ctrl+U cursorUndoAction.run(null!, editor, {}); - assert.deepEqual(editor.getSelections(), [new Selection(1, 5, 1, 5)]); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 5, 1, 5)]); }); }); @@ -52,12 +52,12 @@ suite('FindController', () => { // type hello editor.trigger('test', Handler.Type, { text: 'hell' }); editor.trigger('test', Handler.Type, { text: 'o' }); - assert.deepEqual(editor.getValue(), 'hello'); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(editor.getValue(), 'hello'); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); // press Ctrl+U cursorUndoAction.run(null!, editor, {}); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); }); }); }); diff --git a/src/vs/editor/contrib/dnd/dnd.ts b/src/vs/editor/contrib/dnd/dnd.ts index b1b4a06b7..d535bb906 100644 --- a/src/vs/editor/contrib/dnd/dnd.ts +++ b/src/vs/editor/contrib/dnd/dnd.ts @@ -20,6 +20,7 @@ import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; function hasTriggerModifier(e: IKeyboardEvent | IMouseEvent): boolean { if (isMacintosh) { @@ -176,8 +177,8 @@ export class DragAndDropController extends Disposable implements IEditorContribu } }); } - // Use `mouse` as the source instead of `api`. - (this._editor).setSelections(newSelections || [], 'mouse'); + // Use `mouse` as the source instead of `api` and setting the reason to explicit (to behave like any other mouse operation). + (this._editor).setSelections(newSelections || [], 'mouse', CursorChangeReason.Explicit); } else if (!this._dragSelection.containsPosition(newCursorPosition) || ( ( diff --git a/src/vs/editor/contrib/gotoSymbol/documentSymbols.ts b/src/vs/editor/contrib/documentSymbols/documentSymbols.ts similarity index 54% rename from src/vs/editor/contrib/gotoSymbol/documentSymbols.ts rename to src/vs/editor/contrib/documentSymbols/documentSymbols.ts index 51702b59e..64f5fa0de 100644 --- a/src/vs/editor/contrib/gotoSymbol/documentSymbols.ts +++ b/src/vs/editor/contrib/documentSymbols/documentSymbols.ts @@ -4,62 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { DocumentSymbol } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { assertType } from 'vs/base/common/types'; -import { Iterable } from 'vs/base/common/iterator'; export async function getDocumentSymbols(document: ITextModel, flat: boolean, token: CancellationToken): Promise { - const model = await OutlineModel.create(document, token); - const roots: DocumentSymbol[] = []; - for (const child of model.children.values()) { - if (child instanceof OutlineElement) { - roots.push(child.symbol); - } else { - roots.push(...Iterable.map(child.children.values(), child => child.symbol)); - } - } - - let flatEntries: DocumentSymbol[] = []; - if (token.isCancellationRequested) { - return flatEntries; - } - if (flat) { - flatten(flatEntries, roots, ''); - } else { - flatEntries = roots; - } - - return flatEntries.sort(compareEntriesUsingStart); -} - -function compareEntriesUsingStart(a: DocumentSymbol, b: DocumentSymbol): number { - return Range.compareRangesUsingStarts(a.range, b.range); -} - -function flatten(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void { - for (let entry of entries) { - bucket.push({ - kind: entry.kind, - tags: entry.tags, - name: entry.name, - detail: entry.detail, - containerName: entry.containerName || overrideContainerLabel, - range: entry.range, - selectionRange: entry.selectionRange, - children: undefined, // we flatten it... - }); - if (entry.children) { - flatten(bucket, entry.children, entry.name); - } - } + return flat + ? model.asListOfDocumentSymbols() + : model.getTopLevelSymbols(); } CommandsRegistry.registerCommand('_executeDocumentSymbolProvider', async function (accessor, ...args) { diff --git a/src/vs/editor/contrib/documentSymbols/outline.ts b/src/vs/editor/contrib/documentSymbols/outline.ts deleted file mode 100644 index 1dc949017..000000000 --- a/src/vs/editor/contrib/documentSymbols/outline.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; - -export const OutlineViewId = 'outline'; - -export const OutlineViewFiltered = new RawContextKey('outlineFiltered', false); -export const OutlineViewFocused = new RawContextKey('outlineFocused', false); - -export const enum OutlineConfigKeys { - 'icons' = 'outline.icons', - 'problemsEnabled' = 'outline.problems.enabled', - 'problemsColors' = 'outline.problems.colors', - 'problemsBadges' = 'outline.problems.badges' -} diff --git a/src/vs/editor/contrib/documentSymbols/outlineModel.ts b/src/vs/editor/contrib/documentSymbols/outlineModel.ts index c85081502..5638223f9 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineModel.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineModel.ts @@ -14,8 +14,8 @@ import { ITextModel } from 'vs/editor/common/model'; import { DocumentSymbol, DocumentSymbolProvider, DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; import { MarkerSeverity } from 'vs/platform/markers/common/markers'; import { Iterable } from 'vs/base/common/iterator'; -import { URI } from 'vs/base/common/uri'; import { LanguageFeatureRequestDelays } from 'vs/editor/common/modes/languageFeatureRegistry'; +import { URI } from 'vs/base/common/uri'; export abstract class TreeElement { @@ -204,8 +204,6 @@ export class OutlineGroup extends TreeElement { } } - - export class OutlineModel extends TreeElement { private static readonly _requestDurations = new LanguageFeatureRequestDelays(DocumentSymbolProviderRegistry, 350); @@ -445,4 +443,43 @@ export class OutlineModel extends TreeElement { group.updateMarker(marker.slice(0)); } } + + getTopLevelSymbols(): DocumentSymbol[] { + const roots: DocumentSymbol[] = []; + for (const child of this.children.values()) { + if (child instanceof OutlineElement) { + roots.push(child.symbol); + } else { + roots.push(...Iterable.map(child.children.values(), child => child.symbol)); + } + } + return roots.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + } + + asListOfDocumentSymbols(): DocumentSymbol[] { + const roots = this.getTopLevelSymbols(); + const bucket: DocumentSymbol[] = []; + OutlineModel._flattenDocumentSymbols(bucket, roots, ''); + return bucket.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + } + + private static _flattenDocumentSymbols(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void { + for (const entry of entries) { + bucket.push({ + kind: entry.kind, + tags: entry.tags, + name: entry.name, + detail: entry.detail, + containerName: entry.containerName || overrideContainerLabel, + range: entry.range, + selectionRange: entry.selectionRange, + children: undefined, // we flatten it... + }); + + // Recurse over children + if (entry.children) { + OutlineModel._flattenDocumentSymbols(bucket, entry.children, entry.name); + } + } + } } diff --git a/src/vs/editor/contrib/find/findWidget.css b/src/vs/editor/contrib/find/findWidget.css index 15ed9eb3f..86efb77c2 100644 --- a/src/vs/editor/contrib/find/findWidget.css +++ b/src/vs/editor/contrib/find/findWidget.css @@ -60,14 +60,14 @@ } -.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .mirror { +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .ibwrapper > .mirror { padding-right: 22px; } -.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .input, -.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .mirror, -.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .input, -.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .mirror { +.monaco-editor .find-widget > .find-part .monaco-inputbox > .ibwrapper > .input, +.monaco-editor .find-widget > .find-part .monaco-inputbox > .ibwrapper > .mirror, +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .ibwrapper > .input, +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .ibwrapper > .mirror { padding-top: 2px; padding-bottom: 2px; } diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 597af6c32..89981ba44 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -1044,7 +1044,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL // Toggle selection button this._toggleSelectionFind = this._register(new Checkbox({ - icon: ThemeIcon.asCSSIcon(findSelectionIcon), + icon: findSelectionIcon, title: NLS_TOGGLE_SELECTION_FIND_TITLE + this._keybindingLabelFor(FIND_IDS.ToggleSearchScopeCommand), isChecked: false })); diff --git a/src/vs/editor/contrib/find/test/find.test.ts b/src/vs/editor/contrib/find/test/find.test.ts index 98f31f328..55490860d 100644 --- a/src/vs/editor/contrib/find/test/find.test.ts +++ b/src/vs/editor/contrib/find/test/find.test.ts @@ -20,17 +20,17 @@ suite('Find', () => { // The cursor is at the very top, of the file, at the first ABC let searchStringAtTop = getSelectionSearchString(editor); - assert.equal(searchStringAtTop, 'ABC'); + assert.strictEqual(searchStringAtTop, 'ABC'); // Move cursor to the end of ABC editor.setPosition(new Position(1, 3)); let searchStringAfterABC = getSelectionSearchString(editor); - assert.equal(searchStringAfterABC, 'ABC'); + assert.strictEqual(searchStringAfterABC, 'ABC'); // Move cursor to DEF editor.setPosition(new Position(1, 5)); let searchStringInsideDEF = getSelectionSearchString(editor); - assert.equal(searchStringInsideDEF, 'DEF'); + assert.strictEqual(searchStringInsideDEF, 'DEF'); }); }); @@ -44,17 +44,17 @@ suite('Find', () => { // Select A of ABC editor.setSelection(new Range(1, 1, 1, 2)); let searchStringSelectionA = getSelectionSearchString(editor); - assert.equal(searchStringSelectionA, 'A'); + assert.strictEqual(searchStringSelectionA, 'A'); // Select BC of ABC editor.setSelection(new Range(1, 2, 1, 4)); let searchStringSelectionBC = getSelectionSearchString(editor); - assert.equal(searchStringSelectionBC, 'BC'); + assert.strictEqual(searchStringSelectionBC, 'BC'); // Select BC DE editor.setSelection(new Range(1, 2, 1, 7)); let searchStringSelectionBCDE = getSelectionSearchString(editor); - assert.equal(searchStringSelectionBCDE, 'BC DE'); + assert.strictEqual(searchStringSelectionBCDE, 'BC DE'); }); }); @@ -68,17 +68,17 @@ suite('Find', () => { // Select first line and newline editor.setSelection(new Range(1, 1, 2, 1)); let searchStringSelectionWholeLine = getSelectionSearchString(editor); - assert.equal(searchStringSelectionWholeLine, null); + assert.strictEqual(searchStringSelectionWholeLine, null); // Select first line and chunk of second editor.setSelection(new Range(1, 1, 2, 4)); let searchStringSelectionTwoLines = getSelectionSearchString(editor); - assert.equal(searchStringSelectionTwoLines, null); + assert.strictEqual(searchStringSelectionTwoLines, null); // Select end of first line newline and chunk of second editor.setSelection(new Range(1, 7, 2, 4)); let searchStringSelectionSpanLines = getSelectionSearchString(editor); - assert.equal(searchStringSelectionSpanLines, null); + assert.strictEqual(searchStringSelectionSpanLines, null); }); }); diff --git a/src/vs/editor/contrib/find/test/findController.test.ts b/src/vs/editor/contrib/find/test/findController.test.ts index 8d9216ea0..3f55711c1 100644 --- a/src/vs/editor/contrib/find/test/findController.test.ts +++ b/src/vs/editor/contrib/find/test/findController.test.ts @@ -102,7 +102,7 @@ suite('FindController', async () => { // I hit Ctrl+F to show the Find dialog startFindAction.run(null, editor); - assert.deepEqual(findController.getGlobalBufferTerm(), findController.getState().searchString); + assert.deepStrictEqual(findController.getGlobalBufferTerm(), findController.getState().searchString); findController.dispose(); }); }); @@ -126,9 +126,9 @@ suite('FindController', async () => { let nextMatchFindAction = new NextMatchFindAction(); nextMatchFindAction.run(null, editor); - assert.equal(findState.searchString, 'ABC'); + assert.strictEqual(findState.searchString, 'ABC'); - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); findController.dispose(); }); @@ -152,7 +152,7 @@ suite('FindController', async () => { findState.change({ searchString: 'ABC' }, true); - assert.deepEqual(findController.getGlobalBufferTerm(), 'ABC'); + assert.deepStrictEqual(findController.getGlobalBufferTerm(), 'ABC'); findController.dispose(); }); @@ -181,14 +181,14 @@ suite('FindController', async () => { findState.change({ searchString: 'ABC' }, true); // The first ABC is highlighted. - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); // I hit Esc to exit the Find dialog. findController.closeFindWidget(); findController.hasFocus = false; // The cursor is now at end of the first line, with ABC on that line highlighted. - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); // I hit delete to remove it and change the text to XYZ. editor.pushUndoStop(); @@ -201,16 +201,16 @@ suite('FindController', async () => { // ABC // XYZ // ABC - assert.equal(editor.getModel()!.getLineContent(1), 'XYZ'); + assert.strictEqual(editor.getModel()!.getLineContent(1), 'XYZ'); // The cursor is at end of the first line. - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 4, 1, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 4, 1, 4]); // I hit F3 to "Find Next" to find the next occurrence of ABC, but instead it searches for XYZ. await nextMatchFindAction.run(null, editor); - assert.equal(findState.searchString, 'ABC'); - assert.equal(findController.hasFocus, false); + assert.strictEqual(findState.searchString, 'ABC'); + assert.strictEqual(findController.hasFocus, false); findController.dispose(); }); @@ -230,10 +230,10 @@ suite('FindController', async () => { }); await nextMatchFindAction.run(null, editor); - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 26, 1, 29]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 26, 1, 29]); await nextMatchFindAction.run(null, editor); - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 8, 1, 11]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 8, 1, 11]); findController.dispose(); }); @@ -256,10 +256,10 @@ suite('FindController', async () => { await startFindAction.run(null, editor); await nextMatchFindAction.run(null, editor); - assert.deepEqual(fromSelection(editor.getSelection()!), [2, 9, 2, 13]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [2, 9, 2, 13]); await nextMatchFindAction.run(null, editor); - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 9, 1, 13]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 9, 1, 13]); findController.dispose(); }); @@ -288,7 +288,7 @@ suite('FindController', async () => { await nextMatchFindAction.run(null, editor); await startFindReplaceAction.run(null, editor); - assert.equal(findController.getState().searchString, testRegexString); + assert.strictEqual(findController.getState().searchString, testRegexString); findController.dispose(); }); @@ -312,16 +312,16 @@ suite('FindController', async () => { loop: true }); - assert.equal(findController.getState().searchScope, null); + assert.strictEqual(findController.getState().searchScope, null); findController.getState().change({ searchScope: [new Range(1, 1, 1, 5)] }, false); - assert.deepEqual(findController.getState().searchScope, [new Range(1, 1, 1, 5)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Range(1, 1, 1, 5)]); findController.closeFindWidget(); - assert.equal(findController.getState().searchScope, null); + assert.strictEqual(findController.getState().searchScope, null); }); }); @@ -338,13 +338,13 @@ suite('FindController', async () => { findController.getState().change({ searchString: '\\b\\s{3}\\b', replaceString: ' ', isRegex: true }, false); findController.moveToNextMatch(); - assert.deepEqual(editor.getSelections()!.map(fromSelection), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [ [1, 39, 1, 42] ]); findController.replace(); - assert.deepEqual(editor.getValue(), 'HRESULT OnAmbientPropertyChange(DISPID dispid);'); + assert.deepStrictEqual(editor.getValue(), 'HRESULT OnAmbientPropertyChange(DISPID dispid);'); findController.dispose(); }); @@ -365,13 +365,13 @@ suite('FindController', async () => { findController.getState().change({ searchString: '^', replaceString: 'x', isRegex: true }, false); findController.moveToNextMatch(); - assert.deepEqual(editor.getSelections()!.map(fromSelection), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [ [2, 1, 2, 1] ]); findController.replace(); - assert.deepEqual(editor.getValue(), '\nxline2\nline3'); + assert.deepStrictEqual(editor.getValue(), '\nxline2\nline3'); findController.dispose(); }); @@ -396,7 +396,7 @@ suite('FindController', async () => { // cmd+f3 await nextSelectionMatchFindAction.run(null, editor); - assert.deepEqual(editor.getSelections()!.map(fromSelection), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [ [3, 1, 3, 9] ]); @@ -427,7 +427,7 @@ suite('FindController', async () => { // cmd+f3 await nextSelectionMatchFindAction.run(null, editor); - assert.deepEqual(editor.getSelections()!.map(fromSelection), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [ [3, 1, 3, 9] ]); @@ -458,7 +458,7 @@ suite('FindController', async () => { await startFindWithSelectionAction.run(null, editor); let findState = findController.getState(); - assert.deepEqual(findState.searchString.split(/\r\n|\r|\n/g), ['ABC', 'ABC']); + assert.deepStrictEqual(findState.searchString.split(/\r\n|\r|\n/g), ['ABC', 'ABC']); editor.setSelection(new Selection(3, 1, 3, 1)); await startFindWithSelectionAction.run(null, editor); @@ -483,7 +483,7 @@ suite('FindController', async () => { startFindWithSelectionAction.run(null, editor); let findState = findController.getState(); - assert.deepEqual(findState.searchString, 'ABC'); + assert.deepStrictEqual(findState.searchString, 'ABC'); findController.dispose(); }); }); @@ -531,7 +531,7 @@ suite('FindController query options persistence', async () => { // I type ABC. findState.change({ searchString: 'ABC' }, true); // The second ABC is highlighted as matchCase is true. - assert.deepEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 4]); findController.dispose(); }); @@ -558,7 +558,7 @@ suite('FindController query options persistence', async () => { // I type AB. findState.change({ searchString: 'AB' }, true); // The second AB is highlighted as wholeWord is true. - assert.deepEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 3]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 3]); findController.dispose(); }); @@ -575,7 +575,7 @@ suite('FindController query options persistence', async () => { // The cursor is at the very top, of the file, at the first ABC let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); findController.toggleRegex(); - assert.equal(queryState['editor.isRegex'], true); + assert.strictEqual(queryState['editor.isRegex'], true); findController.dispose(); }); @@ -601,13 +601,13 @@ suite('FindController query options persistence', async () => { editor.setSelection(new Range(1, 1, 2, 1)); findController.start(findConfig); - assert.deepEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1)]); findController.closeFindWidget(); editor.setSelections([new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]); findController.start(findConfig); - assert.deepEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]); }); }); @@ -631,7 +631,7 @@ suite('FindController query options persistence', async () => { loop: true }); - assert.deepEqual(findController.getState().searchScope, null); + assert.deepStrictEqual(findController.getState().searchScope, null); }); }); @@ -655,7 +655,7 @@ suite('FindController query options persistence', async () => { loop: true }); - assert.deepEqual(findController.getState().searchScope, [new Selection(1, 2, 1, 3)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 2, 1, 3)]); }); }); @@ -680,7 +680,7 @@ suite('FindController query options persistence', async () => { loop: true }); - assert.deepEqual(findController.getState().searchScope, [new Selection(1, 6, 2, 1)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 6, 2, 1)]); }); }); }); diff --git a/src/vs/editor/contrib/find/test/findModel.test.ts b/src/vs/editor/contrib/find/test/findModel.test.ts index 70c549498..10c7ae326 100644 --- a/src/vs/editor/contrib/find/test/findModel.test.ts +++ b/src/vs/editor/contrib/find/test/findModel.test.ts @@ -81,13 +81,13 @@ suite('FindModel', () => { } function assertFindState(editor: ICodeEditor, cursor: number[], highlighted: number[] | null, findDecorations: number[][]): void { - assert.deepEqual(fromRange(editor.getSelection()!), cursor, 'cursor'); + assert.deepStrictEqual(fromRange(editor.getSelection()!), cursor, 'cursor'); let expectedState = { highlighted: highlighted ? [highlighted] : [], findDecorations: findDecorations }; - assert.deepEqual(_getFindState(editor), expectedState, 'state'); + assert.deepStrictEqual(_getFindState(editor), expectedState, 'state'); } findTest('incremental find from beginning of file', (editor) => { @@ -245,7 +245,7 @@ suite('FindModel', () => { findState.change({ searchString: 'hello' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); - assert.equal(findState.matchesCount, 5); + assert.strictEqual(findState.matchesCount, 5); assertFindState( editor, [1, 1, 1, 1], @@ -275,7 +275,7 @@ suite('FindModel', () => { findState.change({ searchString: 'hello' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); - assert.equal(findState.matchesCount, 5); + assert.strictEqual(findState.matchesCount, 5); assertFindState( editor, [1, 1, 1, 1], @@ -290,7 +290,7 @@ suite('FindModel', () => { ); findState.change({ searchString: 'helloo' }, false); - assert.equal(findState.matchesCount, 0); + assert.strictEqual(findState.matchesCount, 0); assertFindState( editor, [1, 1, 1, 1], @@ -1306,7 +1306,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1320,7 +1320,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1333,7 +1333,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, hi!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, hi!" << endl;'); findModel.replace(); assertFindState( @@ -1345,7 +1345,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); findModel.replace(); assertFindState( @@ -1356,7 +1356,7 @@ suite('FindModel', () => { [6, 14, 6, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); findModel.replace(); assertFindState( @@ -1365,7 +1365,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, hi!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, hi!" << endl;'); findModel.dispose(); findState.dispose(); @@ -1398,7 +1398,7 @@ suite('FindModel', () => { [11, 10, 11, 13] ] ); - assert.equal(editor.getModel()!.getLineContent(11), '// blablablaciao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// blablablaciao'); findModel.replace(); assertFindState( @@ -1410,7 +1410,7 @@ suite('FindModel', () => { [11, 11, 11, 14] ] ); - assert.equal(editor.getModel()!.getLineContent(11), '// ciaoblablaciao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// ciaoblablaciao'); findModel.replace(); assertFindState( @@ -1421,7 +1421,7 @@ suite('FindModel', () => { [11, 12, 11, 15] ] ); - assert.equal(editor.getModel()!.getLineContent(11), '// ciaociaoblaciao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// ciaociaoblaciao'); findModel.replace(); assertFindState( @@ -1430,7 +1430,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(11), '// ciaociaociaociao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// ciaociaociaociao'); findModel.dispose(); findState.dispose(); @@ -1467,7 +1467,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replaceAll(); assertFindState( @@ -1476,9 +1476,9 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, hi!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, hi!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -1517,10 +1517,10 @@ suite('FindModel', () => { [9, 1, 9, 3] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hello world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Hello world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(9), ' cout << "helloworld again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hello world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Hello world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(9), ' cout << "helloworld again" << endl;'); findModel.dispose(); findState.dispose(); @@ -1549,7 +1549,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(11), '// ciaociaociaociao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// ciaociaociaociao'); findModel.dispose(); findState.dispose(); @@ -1578,10 +1578,10 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(11), '// <'); - assert.equal(editor.getModel()!.getLineContent(12), '\t><'); - assert.equal(editor.getModel()!.getLineContent(13), '\t><'); - assert.equal(editor.getModel()!.getLineContent(14), '\t>ciao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// <'); + assert.strictEqual(editor.getModel()!.getLineContent(12), '\t><'); + assert.strictEqual(editor.getModel()!.getLineContent(13), '\t><'); + assert.strictEqual(editor.getModel()!.getLineContent(14), '\t>ciao'); findModel.dispose(); findState.dispose(); @@ -1610,8 +1610,8 @@ suite('FindModel', () => { [] ); - assert.equal(editor.getModel()!.getLineContent(2), '#bar "cool.h"'); - assert.equal(editor.getModel()!.getLineContent(3), '#bar '); + assert.strictEqual(editor.getModel()!.getLineContent(2), '#bar "cool.h"'); + assert.strictEqual(editor.getModel()!.getLineContent(3), '#bar '); findModel.dispose(); findState.dispose(); @@ -1665,7 +1665,7 @@ suite('FindModel', () => { findModel.selectAllMatches(); - assert.deepEqual(editor!.getSelections()!.map(s => s.toString()), [ + assert.deepStrictEqual(editor!.getSelections()!.map(s => s.toString()), [ new Selection(6, 14, 6, 19), new Selection(6, 27, 6, 32), new Selection(7, 14, 7, 19), @@ -1709,14 +1709,14 @@ suite('FindModel', () => { findModel.selectAllMatches(); - assert.deepEqual(editor!.getSelections()!.map(s => s.toString()), [ + assert.deepStrictEqual(editor!.getSelections()!.map(s => s.toString()), [ new Selection(7, 14, 7, 19), new Selection(6, 14, 6, 19), new Selection(6, 27, 6, 32), new Selection(8, 14, 8, 19) ].map(s => s.toString())); - assert.deepEqual(editor!.getSelection()!.toString(), new Selection(7, 14, 7, 19).toString()); + assert.deepStrictEqual(editor!.getSelection()!.toString(), new Selection(7, 14, 7, 19).toString()); assertFindState( editor, @@ -1800,7 +1800,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1812,7 +1812,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1823,7 +1823,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); findModel.replace(); assertFindState( @@ -1832,7 +1832,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -1871,7 +1871,7 @@ suite('FindModel', () => { ] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Hello world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Hello world again" << endl;'); findModel.replace(); assertFindState( @@ -1883,7 +1883,7 @@ suite('FindModel', () => { [7, 14, 7, 19], ] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); findModel.replace(); assertFindState( @@ -1894,7 +1894,7 @@ suite('FindModel', () => { [7, 14, 7, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1903,7 +1903,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -1927,9 +1927,9 @@ suite('FindModel', () => { findModel.replaceAll(); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); assertFindState( editor, @@ -1970,7 +1970,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1982,7 +1982,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hilo world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hilo world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1993,7 +1993,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hilo world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hilo world again" << endl;'); findModel.replace(); assertFindState( @@ -2002,7 +2002,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hilo world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hilo world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -2027,10 +2027,10 @@ suite('FindModel', () => { findModel.replaceAll(); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello girl, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hello girl again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Hello girl again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(9), ' cout << "hellogirl again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello girl, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hello girl again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Hello girl again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(9), ' cout << "hellogirl again" << endl;'); assertFindState( editor, @@ -2060,8 +2060,8 @@ suite('FindModel', () => { findModel.replaceAll(); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello girl, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Hello girl again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello girl, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Hello girl again" << endl;'); assertFindState( editor, @@ -2094,10 +2094,10 @@ suite('FindModel', () => { findModel.replaceAll(); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "goodbye world, Goodbye!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "goodbye world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Goodbye world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(9), ' cout << "goodbyeworld again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "goodbye world, Goodbye!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "goodbye world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Goodbye world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(9), ' cout << "goodbyeworld again" << endl;'); assertFindState( editor, @@ -2134,9 +2134,9 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << " world, !" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << " world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << " world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << " world, !" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << " world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << " world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -2159,7 +2159,7 @@ suite('FindModel', () => { expectedText += 'a line' + i + '\n'; } expectedText += 'a '; - assert.equal(editor!.getModel()!.getValue(), expectedText); + assert.strictEqual(editor!.getModel()!.getValue(), expectedText); findModel.dispose(); findState.dispose(); @@ -2188,9 +2188,9 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(9), ' cout << "hiworld again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(9), ' cout << "hiworld again" << endl;'); findModel.dispose(); findState.dispose(); @@ -2219,78 +2219,78 @@ suite('FindModel', () => { findState.change({ searchString: 'hello', loop: false }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); - assert.equal(findState.matchesCount, 5); + assert.strictEqual(findState.matchesCount, 5); // Test next operations - assert.equal(findState.matchesPosition, 0); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 0); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), false); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), false); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 3); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 3); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 4); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 4); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), false); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), false); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), false); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), false); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), false); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), false); + assert.strictEqual(findState.canNavigateBack(), true); // Test previous operations findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 4); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 4); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 3); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 3); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), false); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), false); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), false); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), false); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), false); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), false); }); @@ -2299,78 +2299,78 @@ suite('FindModel', () => { findState.change({ searchString: 'hello' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); - assert.equal(findState.matchesCount, 5); + assert.strictEqual(findState.matchesCount, 5); // Test next operations - assert.equal(findState.matchesPosition, 0); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 0); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 3); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 3); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 4); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 4); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); // Test previous operations findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 4); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 4); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 3); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 3); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); }); diff --git a/src/vs/editor/contrib/find/test/replacePattern.test.ts b/src/vs/editor/contrib/find/test/replacePattern.test.ts index 907292fd7..d94918640 100644 --- a/src/vs/editor/contrib/find/test/replacePattern.test.ts +++ b/src/vs/editor/contrib/find/test/replacePattern.test.ts @@ -13,7 +13,7 @@ suite('Replace Pattern test', () => { let testParse = (input: string, expectedPieces: ReplacePiece[]) => { let actual = parseReplaceString(input); let expected = new ReplacePattern(expectedPieces); - assert.deepEqual(actual, expected, 'Parsing ' + input); + assert.deepStrictEqual(actual, expected, 'Parsing ' + input); }; // no backslash => no treatment @@ -73,14 +73,14 @@ suite('Replace Pattern test', () => { let testParse = (input: string, expectedPieces: ReplacePiece[]) => { let actual = parseReplaceString(input); let expected = new ReplacePattern(expectedPieces); - assert.deepEqual(actual, expected, 'Parsing ' + input); + assert.deepStrictEqual(actual, expected, 'Parsing ' + input); }; function assertReplace(target: string, search: RegExp, replaceString: string, expected: string): void { let replacePattern = parseReplaceString(replaceString); let m = search.exec(target); let actual = replacePattern.buildReplaceString(m); - assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); + assert.strictEqual(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); } // \U, \u => uppercase \L, \l => lowercase \E => cancel @@ -107,7 +107,7 @@ suite('Replace Pattern test', () => { let m = search.exec(target); let actual = replacePattern.buildReplaceString(m); - assert.deepEqual(actual, expected, `${target}.replace(${search}, ${replaceString})`); + assert.deepStrictEqual(actual, expected, `${target}.replace(${search}, ${replaceString})`); }; testJSReplaceSemantics('hi', /hi/, 'hello', 'hi'.replace(/hi/, 'hello')); @@ -136,7 +136,7 @@ suite('Replace Pattern test', () => { let m = search.exec(target); let actual = replacePattern.buildReplaceString(m); - assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); + assert.strictEqual(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); } assertReplace('bla', /bla/, 'hello', 'hello'); @@ -162,7 +162,7 @@ suite('Replace Pattern test', () => { let m = search.exec(target); let actual = replacePattern.buildReplaceString(m); - assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); + assert.strictEqual(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); } assertReplace('this is a bla text', /bla/, 'hello', 'hello'); assertReplace('this is a bla text', /this(?=.*bla)/, 'that', 'that'); @@ -184,14 +184,14 @@ suite('Replace Pattern test', () => { let replacePattern = parseReplaceString('a{$1}'); let matches = /a(z)?/.exec('abcd'); let actual = replacePattern.buildReplaceString(matches); - assert.equal(actual, 'a{}'); + assert.strictEqual(actual, 'a{}'); }); test('buildReplaceStringWithCasePreserved test', () => { function assertReplace(target: string[], replaceString: string, expected: string): void { let actual: string = ''; actual = buildReplaceStringWithCasePreserved(target, replaceString); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); } assertReplace(['abc'], 'Def', 'def'); @@ -219,7 +219,7 @@ suite('Replace Pattern test', () => { function assertReplace(target: string[], replaceString: string, expected: string): void { let replacePattern = parseReplaceString(replaceString); let actual = replacePattern.buildReplaceString(target, true); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); } assertReplace(['abc'], 'Def', 'def'); diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index 10a503f67..8096baa17 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -16,13 +16,12 @@ import { Color } from 'vs/base/common/color'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { ScrollType } from 'vs/editor/common/editorCommon'; -import { getBaseLabel, getPathLabel } from 'vs/base/common/labels'; +import { getBaseLabel } from 'vs/base/common/labels'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { Event, Emitter } from 'vs/base/common/event'; import { PeekViewWidget, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/peekView'; import { basename } from 'vs/base/common/resources'; import { IAction } from 'vs/base/common/actions'; -import { IActionBarOptions, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -31,6 +30,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { splitLines } from 'vs/base/common/strings'; +import { ILabelService } from 'vs/platform/label/common/label'; class MessageWidget { @@ -51,6 +51,7 @@ class MessageWidget { editor: ICodeEditor, onRelatedInformation: (related: IRelatedInformation) => void, private readonly _openerService: IOpenerService, + private readonly _labelService: ILabelService ) { this._editor = editor; @@ -169,7 +170,7 @@ class MessageWidget { let relatedResource = document.createElement('a'); relatedResource.classList.add('filename'); relatedResource.innerText = `${getBaseLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `; - relatedResource.title = getPathLabel(related.resource, undefined); + relatedResource.title = this._labelService.getUriLabel(related.resource); this._relatedDiagnostics.set(relatedResource, related); let relatedMessage = document.createElement('span'); @@ -248,7 +249,8 @@ export class MarkerNavigationWidget extends PeekViewWidget { @IOpenerService private readonly _openerService: IOpenerService, @IMenuService private readonly _menuService: IMenuService, @IInstantiationService instantiationService: IInstantiationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ILabelService private readonly _labelService: ILabelService ) { super(editor, { showArrow: true, showFrame: true, isAccessible: true }, instantiationService); this._severity = MarkerSeverity.Warning; @@ -310,13 +312,6 @@ export class MarkerNavigationWidget extends PeekViewWidget { this._icon = dom.append(container, dom.$('')); } - protected _getActionBarOptions(): IActionBarOptions { - return { - ...super._getActionBarOptions(), - orientation: ActionsOrientation.HORIZONTAL - }; - } - protected _fillBody(container: HTMLElement): void { this._parentContainer = container; container.classList.add('marker-widget'); @@ -326,7 +321,7 @@ export class MarkerNavigationWidget extends PeekViewWidget { this._container = document.createElement('div'); container.appendChild(this._container); - this._message = new MessageWidget(this._container, this.editor, related => this._onDidSelectRelatedInformation.fire(related), this._openerService); + this._message = new MessageWidget(this._container, this.editor, related => this._onDidSelectRelatedInformation.fire(related), this._openerService, this._labelService); this._disposables.add(this._message); } diff --git a/src/vs/editor/contrib/gotoSymbol/referencesModel.ts b/src/vs/editor/contrib/gotoSymbol/referencesModel.ts index 334928f3c..63f6b9527 100644 --- a/src/vs/editor/contrib/gotoSymbol/referencesModel.ts +++ b/src/vs/editor/contrib/gotoSymbol/referencesModel.ts @@ -51,7 +51,7 @@ export class OneReference { ); } else { return localize( - 'aria.oneReference.preview', "symbol in {0} on line {1} at column {2}, {3}", + { key: 'aria.oneReference.preview', comment: ['Placeholders are: 0: filename, 1:line number, 2: column number, 3: preview snippet of source code'] }, "symbol in {0} on line {1} at column {2}, {3}", basename(this.uri), this.range.startLineNumber, this.range.startColumn, preview.value ); } diff --git a/src/vs/editor/contrib/hover/hover.ts b/src/vs/editor/contrib/hover/hover.ts index c57fbadbb..071253cbd 100644 --- a/src/vs/editor/contrib/hover/hover.ts +++ b/src/vs/editor/contrib/hover/hover.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IEmptyContentData } from 'vs/editor/browser/controller/mouseTarget'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; @@ -22,11 +22,10 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { IOpenerService } from 'vs/platform/opener/common/opener'; import { editorHoverBackground, editorHoverBorder, editorHoverHighlight, textCodeBlockBackground, textLinkForeground, editorHoverStatusBarBackground, editorHoverForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class ModesHoverController implements IEditorContribution { @@ -35,22 +34,8 @@ export class ModesHoverController implements IEditorContribution { private readonly _toUnhook = new DisposableStore(); private readonly _didChangeConfigurationHandler: IDisposable; - private readonly _contentWidget = new MutableDisposable(); - private readonly _glyphWidget = new MutableDisposable(); - - get contentWidget(): ModesContentHoverWidget { - if (!this._contentWidget.value) { - this._createHoverWidgets(); - } - return this._contentWidget.value!; - } - - get glyphWidget(): ModesGlyphHoverWidget { - if (!this._glyphWidget.value) { - this._createHoverWidgets(); - } - return this._glyphWidget.value!; - } + private _contentWidget: ModesContentHoverWidget | null; + private _glyphWidget: ModesGlyphHoverWidget | null; private _isMouseDown: boolean; private _hoverClicked: boolean; @@ -64,15 +49,16 @@ export class ModesHoverController implements IEditorContribution { } constructor(private readonly _editor: ICodeEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService, @IOpenerService private readonly _openerService: IOpenerService, @IModeService private readonly _modeService: IModeService, - @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, @IThemeService private readonly _themeService: IThemeService, @IContextKeyService _contextKeyService: IContextKeyService ) { this._isMouseDown = false; this._hoverClicked = false; + this._contentWidget = null; + this._glyphWidget = null; this._hookEvents(); @@ -113,8 +99,8 @@ export class ModesHoverController implements IEditorContribution { } private _onModelDecorationsChanged(): void { - this.contentWidget.onModelDecorationsChanged(); - this.glyphWidget.onModelDecorationsChanged(); + this._contentWidget?.onModelDecorationsChanged(); + this._glyphWidget?.onModelDecorationsChanged(); } private _onEditorScrollChanged(e: IScrollEvent): void { @@ -153,7 +139,7 @@ export class ModesHoverController implements IEditorContribution { private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void { let targetType = mouseEvent.target.type; - if (this._isMouseDown && this._hoverClicked && this.contentWidget.isColorPickerVisible()) { + if (this._isMouseDown && this._hoverClicked) { return; } @@ -162,10 +148,14 @@ export class ModesHoverController implements IEditorContribution { return; } + if (this._isHoverSticky && !mouseEvent.event.browserEvent.view?.getSelection()?.isCollapsed) { + // selected text within content hover widget + return; + } if ( !this._isHoverSticky && targetType === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === ModesContentHoverWidget.ID - && this._contentWidget.value?.isColorPickerVisible() + && this._contentWidget?.isColorPickerVisible() ) { // though the hover is not sticky, the color picker needs to. return; @@ -186,26 +176,31 @@ export class ModesHoverController implements IEditorContribution { } if (targetType === MouseTargetType.CONTENT_TEXT) { - this.glyphWidget.hide(); + this._glyphWidget?.hide(); if (this._isHoverEnabled && mouseEvent.target.range) { // TODO@rebornix. This should be removed if we move Color Picker out of Hover component. // Check if mouse is hovering on color decorator const hoverOnColorDecorator = [...mouseEvent.target.element?.classList.values() || []].find(className => className.startsWith('ced-colorBox')) && mouseEvent.target.range.endColumn - mouseEvent.target.range.startColumn === 1; - if (hoverOnColorDecorator) { - // shift the mouse focus by one as color decorator is a `before` decoration of next character. - this.contentWidget.startShowingAt(new Range(mouseEvent.target.range.startLineNumber, mouseEvent.target.range.startColumn + 1, mouseEvent.target.range.endLineNumber, mouseEvent.target.range.endColumn + 1), HoverStartMode.Delayed, false); - } else { - this.contentWidget.startShowingAt(mouseEvent.target.range, HoverStartMode.Delayed, false); + const showAtRange = ( + hoverOnColorDecorator // shift the mouse focus by one as color decorator is a `before` decoration of next character. + ? new Range(mouseEvent.target.range.startLineNumber, mouseEvent.target.range.startColumn + 1, mouseEvent.target.range.endLineNumber, mouseEvent.target.range.endColumn + 1) + : mouseEvent.target.range + ); + if (!this._contentWidget) { + this._contentWidget = new ModesContentHoverWidget(this._editor, this._hoverVisibleKey, this._instantiationService, this._themeService); } - + this._contentWidget.startShowingAt(showAtRange, HoverStartMode.Delayed, false); } } else if (targetType === MouseTargetType.GUTTER_GLYPH_MARGIN) { - this.contentWidget.hide(); + this._contentWidget?.hide(); if (this._isHoverEnabled && mouseEvent.target.position) { - this.glyphWidget.startShowingAt(mouseEvent.target.position.lineNumber); + if (!this._glyphWidget) { + this._glyphWidget = new ModesGlyphHoverWidget(this._editor, this._modeService, this._openerService); + } + this._glyphWidget.startShowingAt(mouseEvent.target.position.lineNumber); } } else { this._hideWidgets(); @@ -220,29 +215,32 @@ export class ModesHoverController implements IEditorContribution { } private _hideWidgets(): void { - if (!this._glyphWidget.value || !this._contentWidget.value || (this._isMouseDown && this._hoverClicked && this._contentWidget.value.isColorPickerVisible())) { + if ((this._isMouseDown && this._hoverClicked && this._contentWidget?.isColorPickerVisible())) { return; } - this._glyphWidget.value.hide(); - this._contentWidget.value.hide(); + this._hoverClicked = false; + this._glyphWidget?.hide(); + this._contentWidget?.hide(); } - private _createHoverWidgets() { - this._contentWidget.value = new ModesContentHoverWidget(this._editor, this._hoverVisibleKey, this._markerDecorationsService, this._keybindingService, this._themeService, this._modeService, this._openerService); - this._glyphWidget.value = new ModesGlyphHoverWidget(this._editor, this._modeService, this._openerService); + public isColorPickerVisible(): boolean { + return this._contentWidget?.isColorPickerVisible() || false; } public showContentHover(range: Range, mode: HoverStartMode, focus: boolean): void { - this.contentWidget.startShowingAt(range, mode, focus); + if (!this._contentWidget) { + this._contentWidget = new ModesContentHoverWidget(this._editor, this._hoverVisibleKey, this._instantiationService, this._themeService); + } + this._contentWidget.startShowingAt(range, mode, focus); } public dispose(): void { this._unhookEvents(); this._toUnhook.dispose(); this._didChangeConfigurationHandler.dispose(); - this._glyphWidget.dispose(); - this._contentWidget.dispose(); + this._glyphWidget?.dispose(); + this._contentWidget?.dispose(); } } diff --git a/src/vs/editor/contrib/hover/hoverWidgets.ts b/src/vs/editor/contrib/hover/hoverWidgets.ts index cb8c02807..ecc374f22 100644 --- a/src/vs/editor/contrib/hover/hoverWidgets.ts +++ b/src/vs/editor/contrib/hover/hoverWidgets.ts @@ -3,168 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Widget } from 'vs/base/browser/ui/widget'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { IContentWidget, ICodeEditor, IContentWidgetPosition, ContentWidgetPositionPreference, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; -import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; -import { renderHoverAction, HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; - -export class ContentHoverWidget extends Widget implements IContentWidget { - - protected readonly _hover: HoverWidget; - private readonly _id: string; - protected _editor: ICodeEditor; - private _isVisible: boolean; - protected _showAtPosition: Position | null; - protected _showAtRange: Range | null; - private _stoleFocus: boolean; - - // Editor.IContentWidget.allowEditorOverflow - public allowEditorOverflow = true; - - protected get isVisible(): boolean { - return this._isVisible; - } - - protected set isVisible(value: boolean) { - this._isVisible = value; - this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); - } - - constructor( - id: string, - editor: ICodeEditor, - private readonly _hoverVisibleKey: IContextKey, - private readonly _keybindingService: IKeybindingService - ) { - super(); - - this._hover = this._register(new HoverWidget()); - this._id = id; - this._editor = editor; - this._isVisible = false; - this._stoleFocus = false; - - this.onkeydown(this._hover.containerDomNode, (e: IKeyboardEvent) => { - if (e.equals(KeyCode.Escape)) { - this.hide(); - } - }); - - this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { - if (e.hasChanged(EditorOption.fontInfo)) { - this.updateFont(); - } - })); - - this._editor.onDidLayoutChange(e => this.layout()); - - this.layout(); - this._editor.addContentWidget(this); - this._showAtPosition = null; - this._showAtRange = null; - this._stoleFocus = false; - } - - public getId(): string { - return this._id; - } - - public getDomNode(): HTMLElement { - return this._hover.containerDomNode; - } - - public showAt(position: Position, range: Range | null, focus: boolean): void { - // Position has changed - this._showAtPosition = position; - this._showAtRange = range; - this._hoverVisibleKey.set(true); - this.isVisible = true; - - this._editor.layoutContentWidget(this); - // Simply force a synchronous render on the editor - // such that the widget does not really render with left = '0px' - this._editor.render(); - this._stoleFocus = focus; - if (focus) { - this._hover.containerDomNode.focus(); - } - } - - public hide(): void { - if (!this.isVisible) { - return; - } - - setTimeout(() => { - // Give commands a chance to see the key - if (!this.isVisible) { - this._hoverVisibleKey.set(false); - } - }, 0); - this.isVisible = false; - - this._editor.layoutContentWidget(this); - if (this._stoleFocus) { - this._editor.focus(); - } - } - - public getPosition(): IContentWidgetPosition | null { - if (this.isVisible) { - return { - position: this._showAtPosition, - range: this._showAtRange, - preference: [ - ContentWidgetPositionPreference.ABOVE, - ContentWidgetPositionPreference.BELOW - ] - }; - } - return null; - } - - public dispose(): void { - this._editor.removeContentWidget(this); - super.dispose(); - } - - private updateFont(): void { - const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code')); - codeClasses.forEach(node => this._editor.applyFontInfo(node)); - } - - protected updateContents(node: Node): void { - this._hover.contentsDomNode.textContent = ''; - this._hover.contentsDomNode.appendChild(node); - this.updateFont(); - - this._editor.layoutContentWidget(this); - this._hover.onContentsChanged(); - } - - protected _renderAction(parent: HTMLElement, actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }): IDisposable { - const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); - const keybindingLabel = keybinding ? keybinding.getLabel() : null; - return renderHoverAction(parent, actionOptions, keybindingLabel); - } - - private layout(): void { - const height = Math.max(this._editor.getLayoutInfo().height / 4, 250); - const { fontSize, lineHeight } = this._editor.getOption(EditorOption.fontInfo); - - this._hover.contentsDomNode.style.fontSize = `${fontSize}px`; - this._hover.contentsDomNode.style.lineHeight = `${lineHeight}px`; - this._hover.contentsDomNode.style.maxHeight = `${height}px`; - this._hover.contentsDomNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`; - } -} export class GlyphHoverWidget extends Widget implements IOverlayWidget { diff --git a/src/vs/editor/contrib/hover/markdownHoverParticipant.ts b/src/vs/editor/contrib/hover/markdownHoverParticipant.ts new file mode 100644 index 000000000..ec4442dbd --- /dev/null +++ b/src/vs/editor/contrib/hover/markdownHoverParticipant.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { IMarkdownString, MarkdownString, isEmptyMarkdownString, markedStringsEquals } from 'vs/base/common/htmlContent'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { asArray } from 'vs/base/common/arrays'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { IEditorHover, IEditorHoverParticipant, IHoverPart } from 'vs/editor/contrib/hover/modesContentHover'; +import { HoverProviderRegistry } from 'vs/editor/common/modes'; +import { getHover } from 'vs/editor/contrib/hover/getHover'; +import { Position } from 'vs/editor/common/core/position'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +const $ = dom.$; + +export class MarkdownHover implements IHoverPart { + + constructor( + public readonly range: Range, + public readonly contents: IMarkdownString[] + ) { } + + public equals(other: IHoverPart): boolean { + if (other instanceof MarkdownHover) { + return markedStringsEquals(this.contents, other.contents); + } + return false; + } +} + +export class MarkdownHoverParticipant implements IEditorHoverParticipant { + + constructor( + private readonly _editor: ICodeEditor, + private readonly _hover: IEditorHover, + @IModeService private readonly _modeService: IModeService, + @IOpenerService private readonly _openerService: IOpenerService, + ) { } + + public createLoadingMessage(range: Range): MarkdownHover { + return new MarkdownHover(range, [new MarkdownString().appendText(nls.localize('modesContentHover.loading', "Loading..."))]); + } + + public computeSync(hoverRange: Range, lineDecorations: IModelDecoration[]): MarkdownHover[] { + if (!this._editor.hasModel()) { + return []; + } + + const model = this._editor.getModel(); + const lineNumber = hoverRange.startLineNumber; + const maxColumn = model.getLineMaxColumn(lineNumber); + const result: MarkdownHover[] = []; + for (const d of lineDecorations) { + const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1; + const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn; + + const hoverMessage = d.options.hoverMessage; + if (!hoverMessage || isEmptyMarkdownString(hoverMessage)) { + continue; + } + + const range = new Range(hoverRange.startLineNumber, startColumn, hoverRange.startLineNumber, endColumn); + result.push(new MarkdownHover(range, asArray(hoverMessage))); + } + + return result; + } + + public async computeAsync(range: Range, token: CancellationToken): Promise { + if (!this._editor.hasModel() || !range) { + return Promise.resolve([]); + } + + const model = this._editor.getModel(); + + if (!HoverProviderRegistry.has(model)) { + return Promise.resolve([]); + } + + const hovers = await getHover(model, new Position( + range.startLineNumber, + range.startColumn + ), token); + + const result: MarkdownHover[] = []; + for (const hover of hovers) { + if (isEmptyMarkdownString(hover.contents)) { + continue; + } + const rng = hover.range ? Range.lift(hover.range) : range; + result.push(new MarkdownHover(rng, hover.contents)); + } + return result; + } + + public renderHoverParts(hoverParts: MarkdownHover[], fragment: DocumentFragment): IDisposable { + const disposables = new DisposableStore(); + for (const hoverPart of hoverParts) { + for (const contents of hoverPart.contents) { + if (isEmptyMarkdownString(contents)) { + continue; + } + const markdownHoverElement = $('div.hover-row.markdown-hover'); + const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents')); + const renderer = disposables.add(new MarkdownRenderer({ editor: this._editor }, this._modeService, this._openerService)); + disposables.add(renderer.onDidRenderAsync(() => { + hoverContentsElement.className = 'hover-contents code-hover-contents'; + this._hover.onContentsChanged(); + })); + const renderedContents = disposables.add(renderer.render(contents)); + hoverContentsElement.appendChild(renderedContents.element); + fragment.appendChild(markdownHoverElement); + } + } + return disposables; + } +} diff --git a/src/vs/editor/contrib/hover/markerHoverParticipant.ts b/src/vs/editor/contrib/hover/markerHoverParticipant.ts new file mode 100644 index 000000000..ad739c892 --- /dev/null +++ b/src/vs/editor/contrib/hover/markerHoverParticipant.ts @@ -0,0 +1,257 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { IDisposable, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { CodeActionTriggerType } from 'vs/editor/common/modes'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { IMarker, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { basename } from 'vs/base/common/resources'; +import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { MarkerController, NextMarkerAction } from 'vs/editor/contrib/gotoError/gotoError'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; +import { getCodeActions, CodeActionSet } from 'vs/editor/contrib/codeAction/codeAction'; +import { QuickFixAction, QuickFixController } from 'vs/editor/contrib/codeAction/codeActionCommands'; +import { CodeActionKind, CodeActionTrigger } from 'vs/editor/contrib/codeAction/types'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Progress } from 'vs/platform/progress/common/progress'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { renderHoverAction } from 'vs/base/browser/ui/hover/hoverWidget'; +import { IEditorHover, IEditorHoverParticipant, IHoverPart } from 'vs/editor/contrib/hover/modesContentHover'; + +const $ = dom.$; + +export class MarkerHover implements IHoverPart { + + constructor( + public readonly range: Range, + public readonly marker: IMarker, + ) { } + + public equals(other: IHoverPart): boolean { + if (other instanceof MarkerHover) { + return IMarkerData.makeKey(this.marker) === IMarkerData.makeKey(other.marker); + } + return false; + } +} + +const markerCodeActionTrigger: CodeActionTrigger = { + type: CodeActionTriggerType.Manual, + filter: { include: CodeActionKind.QuickFix } +}; + +export class MarkerHoverParticipant implements IEditorHoverParticipant { + + private recentMarkerCodeActionsInfo: { marker: IMarker, hasCodeActions: boolean } | undefined = undefined; + + constructor( + private readonly _editor: ICodeEditor, + private readonly _hover: IEditorHover, + @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IOpenerService private readonly _openerService: IOpenerService, + ) { } + + public computeSync(hoverRange: Range, lineDecorations: IModelDecoration[]): MarkerHover[] { + if (!this._editor.hasModel()) { + return []; + } + + const model = this._editor.getModel(); + const lineNumber = hoverRange.startLineNumber; + const maxColumn = model.getLineMaxColumn(lineNumber); + const result: MarkerHover[] = []; + for (const d of lineDecorations) { + const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1; + const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn; + + const marker = this._markerDecorationsService.getMarker(model.uri, d); + if (!marker) { + continue; + } + + const range = new Range(hoverRange.startLineNumber, startColumn, hoverRange.startLineNumber, endColumn); + result.push(new MarkerHover(range, marker)); + } + + return result; + } + + public renderHoverParts(hoverParts: MarkerHover[], fragment: DocumentFragment): IDisposable { + if (!hoverParts.length) { + return Disposable.None; + } + const disposables = new DisposableStore(); + hoverParts.forEach(msg => fragment.appendChild(this.renderMarkerHover(msg, disposables))); + const markerHoverForStatusbar = hoverParts.length === 1 ? hoverParts[0] : hoverParts.sort((a, b) => MarkerSeverity.compare(a.marker.severity, b.marker.severity))[0]; + fragment.appendChild(this.renderMarkerStatusbar(markerHoverForStatusbar, disposables)); + return disposables; + } + + private renderMarkerHover(markerHover: MarkerHover, disposables: DisposableStore): HTMLElement { + const hoverElement = $('div.hover-row'); + const markerElement = dom.append(hoverElement, $('div.marker.hover-contents')); + const { source, message, code, relatedInformation } = markerHover.marker; + + this._editor.applyFontInfo(markerElement); + const messageElement = dom.append(markerElement, $('span')); + messageElement.style.whiteSpace = 'pre-wrap'; + messageElement.innerText = message; + + if (source || code) { + // Code has link + if (code && typeof code !== 'string') { + const sourceAndCodeElement = $('span'); + if (source) { + const sourceElement = dom.append(sourceAndCodeElement, $('span')); + sourceElement.innerText = source; + } + const codeLink = dom.append(sourceAndCodeElement, $('a.code-link')); + codeLink.setAttribute('href', code.target.toString()); + + disposables.add(dom.addDisposableListener(codeLink, 'click', (e) => { + this._openerService.open(code.target); + e.preventDefault(); + e.stopPropagation(); + })); + + const codeElement = dom.append(codeLink, $('span')); + codeElement.innerText = code.value; + + const detailsElement = dom.append(markerElement, sourceAndCodeElement); + detailsElement.style.opacity = '0.6'; + detailsElement.style.paddingLeft = '6px'; + } else { + const detailsElement = dom.append(markerElement, $('span')); + detailsElement.style.opacity = '0.6'; + detailsElement.style.paddingLeft = '6px'; + detailsElement.innerText = source && code ? `${source}(${code})` : source ? source : `(${code})`; + } + } + + if (isNonEmptyArray(relatedInformation)) { + for (const { message, resource, startLineNumber, startColumn } of relatedInformation) { + const relatedInfoContainer = dom.append(markerElement, $('div')); + relatedInfoContainer.style.marginTop = '8px'; + const a = dom.append(relatedInfoContainer, $('a')); + a.innerText = `${basename(resource)}(${startLineNumber}, ${startColumn}): `; + a.style.cursor = 'pointer'; + disposables.add(dom.addDisposableListener(a, 'click', (e) => { + e.stopPropagation(); + e.preventDefault(); + if (this._openerService) { + this._openerService.open(resource, { + fromUserGesture: true, + editorOptions: { selection: { startLineNumber, startColumn } } + }).catch(onUnexpectedError); + } + })); + const messageElement = dom.append(relatedInfoContainer, $('span')); + messageElement.innerText = message; + this._editor.applyFontInfo(messageElement); + } + } + + return hoverElement; + } + + private renderMarkerStatusbar(markerHover: MarkerHover, disposables: DisposableStore): HTMLElement { + const hoverElement = $('div.hover-row.status-bar'); + const actionsElement = dom.append(hoverElement, $('div.actions')); + if (markerHover.marker.severity === MarkerSeverity.Error || markerHover.marker.severity === MarkerSeverity.Warning || markerHover.marker.severity === MarkerSeverity.Info) { + disposables.add(this.renderAction(actionsElement, { + label: nls.localize('peek problem', "Peek Problem"), + commandId: NextMarkerAction.ID, + run: () => { + this._hover.hide(); + MarkerController.get(this._editor).showAtMarker(markerHover.marker); + this._editor.focus(); + } + })); + } + + if (!this._editor.getOption(EditorOption.readOnly)) { + const quickfixPlaceholderElement = dom.append(actionsElement, $('div')); + if (this.recentMarkerCodeActionsInfo) { + if (IMarkerData.makeKey(this.recentMarkerCodeActionsInfo.marker) === IMarkerData.makeKey(markerHover.marker)) { + if (!this.recentMarkerCodeActionsInfo.hasCodeActions) { + quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available"); + } + } else { + this.recentMarkerCodeActionsInfo = undefined; + } + } + const updatePlaceholderDisposable = this.recentMarkerCodeActionsInfo && !this.recentMarkerCodeActionsInfo.hasCodeActions ? Disposable.None : disposables.add(disposableTimeout(() => quickfixPlaceholderElement.textContent = nls.localize('checkingForQuickFixes', "Checking for quick fixes..."), 200)); + if (!quickfixPlaceholderElement.textContent) { + // Have some content in here to avoid flickering + quickfixPlaceholderElement.textContent = String.fromCharCode(0xA0); //   + } + const codeActionsPromise = this.getCodeActions(markerHover.marker); + disposables.add(toDisposable(() => codeActionsPromise.cancel())); + codeActionsPromise.then(actions => { + updatePlaceholderDisposable.dispose(); + this.recentMarkerCodeActionsInfo = { marker: markerHover.marker, hasCodeActions: actions.validActions.length > 0 }; + + if (!this.recentMarkerCodeActionsInfo.hasCodeActions) { + actions.dispose(); + quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available"); + return; + } + quickfixPlaceholderElement.style.display = 'none'; + + let showing = false; + disposables.add(toDisposable(() => { + if (!showing) { + actions.dispose(); + } + })); + + disposables.add(this.renderAction(actionsElement, { + label: nls.localize('quick fixes', "Quick Fix..."), + commandId: QuickFixAction.Id, + run: (target) => { + showing = true; + const controller = QuickFixController.get(this._editor); + const elementPosition = dom.getDomNodePagePosition(target); + // Hide the hover pre-emptively, otherwise the editor can close the code actions + // context menu as well when using keyboard navigation + this._hover.hide(); + controller.showCodeActions(markerCodeActionTrigger, actions, { + x: elementPosition.left + 6, + y: elementPosition.top + elementPosition.height + 6 + }); + } + })); + }); + } + + return hoverElement; + } + + private renderAction(parent: HTMLElement, actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }): IDisposable { + const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); + const keybindingLabel = keybinding ? keybinding.getLabel() : null; + return renderHoverAction(parent, actionOptions, keybindingLabel); + } + + private getCodeActions(marker: IMarker): CancelablePromise { + return createCancelablePromise(cancellationToken => { + return getCodeActions( + this._editor.getModel()!, + new Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn), + markerCodeActionTrigger, + Progress.None, + cancellationToken); + }); + } +} diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index ad2d6b272..ee10d9b80 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -3,228 +3,242 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Color, RGBA } from 'vs/base/common/color'; -import { IMarkdownString, MarkdownString, isEmptyMarkdownString, markedStringsEquals } from 'vs/base/common/htmlContent'; -import { IDisposable, toDisposable, DisposableStore, combinedDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IDisposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { Range } from 'vs/editor/common/core/range'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { DocumentColorProvider, Hover as MarkdownHover, HoverProviderRegistry, IColor, TokenizationRegistry, CodeActionTriggerType } from 'vs/editor/common/modes'; +import { DocumentColorProvider, IColor, TokenizationRegistry } from 'vs/editor/common/modes'; import { getColorPresentations } from 'vs/editor/contrib/colorPicker/color'; import { ColorDetector } from 'vs/editor/contrib/colorPicker/colorDetector'; import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/colorPickerModel'; import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/colorPickerWidget'; -import { getHover } from 'vs/editor/contrib/hover/getHover'; import { HoverOperation, HoverStartMode, IHoverComputer } from 'vs/editor/contrib/hover/hoverOperation'; -import { ContentHoverWidget } from 'vs/editor/contrib/hover/hoverWidgets'; -import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { coalesce, isNonEmptyArray, asArray } from 'vs/base/common/arrays'; -import { IMarker, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { basename } from 'vs/base/common/resources'; -import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener'; -import { MarkerController, NextMarkerAction } from 'vs/editor/contrib/gotoError/gotoError'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; -import { getCodeActions, CodeActionSet } from 'vs/editor/contrib/codeAction/codeAction'; -import { QuickFixAction, QuickFixController } from 'vs/editor/contrib/codeAction/codeActionCommands'; -import { CodeActionKind, CodeActionTrigger } from 'vs/editor/contrib/codeAction/types'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { IIdentifiedSingleEditOperation, TrackedRangeStickiness } from 'vs/editor/common/model'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { coalesce } from 'vs/base/common/arrays'; +import { IIdentifiedSingleEditOperation, IModelDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Constants } from 'vs/base/common/uint'; import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; -import { Progress } from 'vs/platform/progress/common/progress'; import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { Widget } from 'vs/base/browser/ui/widget'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; +import { MarkerHover, MarkerHoverParticipant } from 'vs/editor/contrib/hover/markerHoverParticipant'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { MarkdownHover, MarkdownHoverParticipant } from 'vs/editor/contrib/hover/markdownHoverParticipant'; -const $ = dom.$; +export interface IHoverPart { + readonly range: Range; + equals(other: IHoverPart): boolean; +} -class ColorHover { +export interface IEditorHover { + hide(): void; + onContentsChanged(): void; +} + +export interface IEditorHoverParticipant { + computeSync(hoverRange: Range, lineDecorations: IModelDecoration[]): T[]; + computeAsync?(range: Range, token: CancellationToken): Promise; + renderHoverParts(hoverParts: T[], fragment: DocumentFragment): IDisposable; +} + +class ColorHover implements IHoverPart { constructor( - public readonly range: IRange, + public readonly range: Range, public readonly color: IColor, public readonly provider: DocumentColorProvider ) { } + + equals(other: IHoverPart): boolean { + return false; + } } -class MarkerHover { - +class HoverPartInfo { constructor( - public readonly range: IRange, - public readonly marker: IMarker, + public readonly owner: IEditorHoverParticipant | null, + public readonly data: IHoverPart ) { } } -type HoverPart = MarkdownHover | ColorHover | MarkerHover; - -class ModesContentComputer implements IHoverComputer { +class ModesContentComputer implements IHoverComputer { private readonly _editor: ICodeEditor; - private _result: HoverPart[]; - private _range?: Range; + private _result: HoverPartInfo[]; + private _range: Range | null; constructor( editor: ICodeEditor, - private readonly _markerDecorationsService: IMarkerDecorationsService + private readonly _markerHoverParticipant: IEditorHoverParticipant, + private readonly _markdownHoverParticipant: MarkdownHoverParticipant ) { this._editor = editor; this._result = []; + this._range = null; } - setRange(range: Range): void { + public setRange(range: Range): void { this._range = range; this._result = []; } - clearResult(): void { + public clearResult(): void { this._result = []; } - computeAsync(token: CancellationToken): Promise { + public async computeAsync(token: CancellationToken): Promise { if (!this._editor.hasModel() || !this._range) { return Promise.resolve([]); } - const model = this._editor.getModel(); - - if (!HoverProviderRegistry.has(model)) { - return Promise.resolve([]); - } - - return getHover(model, new Position( - this._range.startLineNumber, - this._range.startColumn - ), token); + const markdownHovers = await this._markdownHoverParticipant.computeAsync(this._range, token); + return markdownHovers.map(h => new HoverPartInfo(this._markdownHoverParticipant, h)); } - computeSync(): HoverPart[] { + public computeSync(): HoverPartInfo[] { if (!this._editor.hasModel() || !this._range) { return []; } const model = this._editor.getModel(); - const lineNumber = this._range.startLineNumber; + const hoverRange = this._range; + const lineNumber = hoverRange.startLineNumber; if (lineNumber > this._editor.getModel().getLineCount()) { // Illegal line number => no results return []; } - const colorDetector = ColorDetector.get(this._editor); const maxColumn = model.getLineMaxColumn(lineNumber); - const lineDecorations = this._editor.getLineDecorations(lineNumber); - let didFindColor = false; - - const hoverRange = this._range; - const result = lineDecorations.map((d): HoverPart | null => { + const lineDecorations = this._editor.getLineDecorations(lineNumber).filter((d) => { const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1; const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn; - if (startColumn > hoverRange.startColumn || hoverRange.endColumn > endColumn) { - return null; - } - - const range = new Range(hoverRange.startLineNumber, startColumn, hoverRange.startLineNumber, endColumn); - const marker = this._markerDecorationsService.getMarker(model, d); - if (marker) { - return new MarkerHover(range, marker); - } - - const colorData = colorDetector.getColorData(d.range.getStartPosition()); - - if (!didFindColor && colorData) { - didFindColor = true; - - const { color, range } = colorData.colorInfo; - return new ColorHover(range, color, colorData.provider); - } else { - if (isEmptyMarkdownString(d.options.hoverMessage)) { - return null; - } - - const contents: IMarkdownString[] = d.options.hoverMessage ? asArray(d.options.hoverMessage) : []; - return { contents, range }; + return false; } + return true; }); + let result: HoverPartInfo[] = []; + + const colorDetector = ColorDetector.get(this._editor); + for (const d of lineDecorations) { + const colorData = colorDetector.getColorData(d.range.getStartPosition()); + if (colorData) { + const { color, range } = colorData.colorInfo; + result.push(new HoverPartInfo(null, new ColorHover(Range.lift(range), color, colorData.provider))); + break; + } + } + + const markdownHovers = this._markdownHoverParticipant.computeSync(this._range, lineDecorations); + result = result.concat(markdownHovers.map(h => new HoverPartInfo(this._markdownHoverParticipant, h))); + + const markerHovers = this._markerHoverParticipant.computeSync(this._range, lineDecorations); + result = result.concat(markerHovers.map(h => new HoverPartInfo(this._markerHoverParticipant, h))); + return coalesce(result); } - onResult(result: HoverPart[], isFromSynchronousComputation: boolean): void { + public onResult(result: HoverPartInfo[], isFromSynchronousComputation: boolean): void { // Always put synchronous messages before asynchronous ones if (isFromSynchronousComputation) { - this._result = result.concat(this._result.sort((a, b) => { - if (a instanceof ColorHover) { // sort picker messages at to the top - return -1; - } else if (b instanceof ColorHover) { - return 1; - } - return 0; - })); + this._result = result.concat(this._result); } else { this._result = this._result.concat(result); } } - getResult(): HoverPart[] { + public getResult(): HoverPartInfo[] { return this._result.slice(0); } - getResultWithLoadingMessage(): HoverPart[] { - return this._result.slice(0).concat([this._getLoadingMessage()]); - } + public getResultWithLoadingMessage(): HoverPartInfo[] { + if (this._range) { + const loadingMessage = new HoverPartInfo(this._markdownHoverParticipant, this._markdownHoverParticipant.createLoadingMessage(this._range)); + return this._result.slice(0).concat([loadingMessage]); - private _getLoadingMessage(): HoverPart { - return { - range: this._range, - contents: [new MarkdownString().appendText(nls.localize('modesContentHover.loading', "Loading..."))] - }; + } + return this._result.slice(0); } } -const markerCodeActionTrigger: CodeActionTrigger = { - type: CodeActionTriggerType.Manual, - filter: { include: CodeActionKind.QuickFix } -}; - -export class ModesContentHoverWidget extends ContentHoverWidget { +export class ModesContentHoverWidget extends Widget implements IContentWidget, IEditorHover { static readonly ID = 'editor.contrib.modesContentHoverWidget'; - private _messages: HoverPart[]; + private readonly _markerHoverParticipant: IEditorHoverParticipant; + private readonly _markdownHoverParticipant: MarkdownHoverParticipant; + + private readonly _hover: HoverWidget; + private readonly _id: string; + private readonly _editor: ICodeEditor; + private _isVisible: boolean; + private _showAtPosition: Position | null; + private _showAtRange: Range | null; + private _stoleFocus: boolean; + + // IContentWidget.allowEditorOverflow + public readonly allowEditorOverflow = true; + + private _messages: HoverPartInfo[]; private _lastRange: Range | null; private readonly _computer: ModesContentComputer; - private readonly _hoverOperation: HoverOperation; + private readonly _hoverOperation: HoverOperation; private _highlightDecorations: string[]; private _isChangingDecorations: boolean; private _shouldFocus: boolean; private _colorPicker: ColorPickerWidget | null; - - private _codeLink?: HTMLElement; - - private readonly renderDisposable = this._register(new MutableDisposable()); + private _renderDisposable: IDisposable | null; constructor( editor: ICodeEditor, - _hoverVisibleKey: IContextKey, - markerDecorationsService: IMarkerDecorationsService, - keybindingService: IKeybindingService, + private readonly _hoverVisibleKey: IContextKey, + instantiationService: IInstantiationService, private readonly _themeService: IThemeService, - private readonly _modeService: IModeService, - private readonly _openerService: IOpenerService = NullOpenerService, ) { - super(ModesContentHoverWidget.ID, editor, _hoverVisibleKey, keybindingService); + super(); + + this._markerHoverParticipant = instantiationService.createInstance(MarkerHoverParticipant, editor, this); + this._markdownHoverParticipant = instantiationService.createInstance(MarkdownHoverParticipant, editor, this); + + this._hover = this._register(new HoverWidget()); + this._id = ModesContentHoverWidget.ID; + this._editor = editor; + this._isVisible = false; + this._stoleFocus = false; + this._renderDisposable = null; + + this.onkeydown(this._hover.containerDomNode, (e: IKeyboardEvent) => { + if (e.equals(KeyCode.Escape)) { + this.hide(); + } + }); + + this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + if (e.hasChanged(EditorOption.fontInfo)) { + this._updateFont(); + } + })); + + this._editor.onDidLayoutChange(() => this.layout()); + + this.layout(); + this._editor.addContentWidget(this); + this._showAtPosition = null; + this._showAtRange = null; + this._stoleFocus = false; this._messages = []; this._lastRange = null; - this._computer = new ModesContentComputer(this._editor, markerDecorationsService); + this._computer = new ModesContentComputer(this._editor, this._markerHoverParticipant, this._markdownHoverParticipant); this._highlightDecorations = []; this._isChangingDecorations = false; this._shouldFocus = false; @@ -246,15 +260,15 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._register(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.BLUR, () => { this.getDomNode().classList.remove('colorpicker-hover'); })); - this._register(editor.onDidChangeConfiguration((e) => { + this._register(editor.onDidChangeConfiguration(() => { this._hoverOperation.setHoverTime(this._editor.getOption(EditorOption.hover).delay); })); - this._register(TokenizationRegistry.onDidChange((e) => { - if (this.isVisible && this._lastRange && this._messages.length > 0) { + this._register(TokenizationRegistry.onDidChange(() => { + if (this._isVisible && this._lastRange && this._messages.length > 0) { this._messages = this._messages.map(msg => { // If a color hover is visible, we need to update the message that // created it so that the color matches the last chosen color - if (msg instanceof ColorHover && !!this._lastRange?.intersectRanges(msg.range) && this._colorPicker?.model.color) { + if (msg.data instanceof ColorHover && !!this._lastRange?.intersectRanges(msg.data.range) && this._colorPicker?.model.color) { const color = this._colorPicker.model.color; const newColor = { red: color.rgba.r / 255, @@ -262,7 +276,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { blue: color.rgba.b / 255, alpha: color.rgba.a }; - return new ColorHover(msg.range, newColor, msg.provider); + return new HoverPartInfo(msg.owner, new ColorHover(msg.data.range, newColor, msg.data.provider)); } else { return msg; } @@ -274,16 +288,81 @@ export class ModesContentHoverWidget extends ContentHoverWidget { })); } - dispose(): void { + public dispose(): void { this._hoverOperation.cancel(); + this._editor.removeContentWidget(this); super.dispose(); } - onModelDecorationsChanged(): void { + public getId(): string { + return this._id; + } + + public getDomNode(): HTMLElement { + return this._hover.containerDomNode; + } + + public showAt(position: Position, range: Range | null, focus: boolean): void { + // Position has changed + this._showAtPosition = position; + this._showAtRange = range; + this._hoverVisibleKey.set(true); + this._isVisible = true; + this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); + + this._editor.layoutContentWidget(this); + // Simply force a synchronous render on the editor + // such that the widget does not really render with left = '0px' + this._editor.render(); + this._stoleFocus = focus; + if (focus) { + this._hover.containerDomNode.focus(); + } + } + + public getPosition(): IContentWidgetPosition | null { + if (this._isVisible) { + return { + position: this._showAtPosition, + range: this._showAtRange, + preference: [ + ContentWidgetPositionPreference.ABOVE, + ContentWidgetPositionPreference.BELOW + ] + }; + } + return null; + } + + private _updateFont(): void { + const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code')); + codeClasses.forEach(node => this._editor.applyFontInfo(node)); + } + + private _updateContents(node: Node): void { + this._hover.contentsDomNode.textContent = ''; + this._hover.contentsDomNode.appendChild(node); + this._updateFont(); + + this._editor.layoutContentWidget(this); + this._hover.onContentsChanged(); + } + + private layout(): void { + const height = Math.max(this._editor.getLayoutInfo().height / 4, 250); + const { fontSize, lineHeight } = this._editor.getOption(EditorOption.fontInfo); + + this._hover.contentsDomNode.style.fontSize = `${fontSize}px`; + this._hover.contentsDomNode.style.lineHeight = `${lineHeight}px`; + this._hover.contentsDomNode.style.maxHeight = `${height}px`; + this._hover.contentsDomNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`; + } + + public onModelDecorationsChanged(): void { if (this._isChangingDecorations) { return; } - if (this.isVisible) { + if (this._isVisible) { // The decorations have changed and the hover is visible, // we need to recompute the displayed text this._hoverOperation.cancel(); @@ -295,7 +374,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { } } - startShowingAt(range: Range, mode: HoverStartMode, focus: boolean): void { + public startShowingAt(range: Range, mode: HoverStartMode, focus: boolean): void { if (this._lastRange && this._lastRange.equalsRange(range)) { // We have to show the widget at the exact same range as before, so no work is needed return; @@ -303,17 +382,17 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._hoverOperation.cancel(); - if (this.isVisible) { + if (this._isVisible) { // The range might have changed, but the hover is visible // Instead of hiding it completely, filter out messages that are still in the new range and // kick off a new computation if (!this._showAtPosition || this._showAtPosition.lineNumber !== range.startLineNumber) { this.hide(); } else { - let filteredMessages: HoverPart[] = []; + let filteredMessages: HoverPartInfo[] = []; for (let i = 0, len = this._messages.length; i < len; i++) { const msg = this._messages[i]; - const rng = msg.range; + const rng = msg.data.range; if (rng && rng.startColumn <= range.startColumn && rng.endColumn >= range.endColumn) { filteredMessages.push(msg); } @@ -335,25 +414,45 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._hoverOperation.start(mode); } - hide(): void { + public hide(): void { this._lastRange = null; this._hoverOperation.cancel(); - super.hide(); + + if (this._isVisible) { + setTimeout(() => { + // Give commands a chance to see the key + if (!this._isVisible) { + this._hoverVisibleKey.set(false); + } + }, 0); + this._isVisible = false; + this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); + + this._editor.layoutContentWidget(this); + if (this._stoleFocus) { + this._editor.focus(); + } + } + this._isChangingDecorations = true; this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, []); this._isChangingDecorations = false; - this.renderDisposable.clear(); + if (this._renderDisposable) { + this._renderDisposable.dispose(); + this._renderDisposable = null; + } this._colorPicker = null; } - isColorPickerVisible(): boolean { - if (this._colorPicker) { - return true; - } - return false; + public isColorPickerVisible(): boolean { + return !!this._colorPicker; } - private _withResult(result: HoverPart[], complete: boolean): void { + public onContentsChanged(): void { + this._hover.onContentsChanged(); + } + + private _withResult(result: HoverPartInfo[], complete: boolean): void { this._messages = result; if (this._lastRange && this._messages.length > 0) { @@ -363,20 +462,24 @@ export class ModesContentHoverWidget extends ContentHoverWidget { } } - private _renderMessages(renderRange: Range, messages: HoverPart[]): void { - this.renderDisposable.dispose(); + private _renderMessages(renderRange: Range, messages: HoverPartInfo[]): void { + if (this._renderDisposable) { + this._renderDisposable.dispose(); + this._renderDisposable = null; + } this._colorPicker = null; // update column from which to show let renderColumn = Constants.MAX_SAFE_SMALL_INTEGER; - let highlightRange: Range | null = messages[0].range ? Range.lift(messages[0].range) : null; + let highlightRange: Range | null = messages[0].data.range ? Range.lift(messages[0].data.range) : null; let fragment = document.createDocumentFragment(); - let isEmptyHoverContent = true; let containColorPicker = false; - const markdownDisposeables = new DisposableStore(); + const disposables = new DisposableStore(); const markerMessages: MarkerHover[] = []; - messages.forEach((msg) => { + const markdownParts: MarkdownHover[] = []; + messages.forEach((_msg) => { + const msg = _msg.data; if (!msg.range) { return; } @@ -464,46 +567,37 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._colorPicker = widget; this.showAt(range.getStartPosition(), range, this._shouldFocus); - this.updateContents(fragment); + this._updateContents(fragment); this._colorPicker.layout(); - this.renderDisposable.value = combinedDisposable(colorListener, colorChangeListener, widget, markdownDisposeables); + this._renderDisposable = combinedDisposable(colorListener, colorChangeListener, widget, disposables); }); } else { if (msg instanceof MarkerHover) { markerMessages.push(msg); - isEmptyHoverContent = false; } else { - msg.contents - .filter(contents => !isEmptyMarkdownString(contents)) - .forEach(contents => { - const markdownHoverElement = $('div.hover-row.markdown-hover'); - const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents')); - const renderer = markdownDisposeables.add(new MarkdownRenderer({ editor: this._editor }, this._modeService, this._openerService)); - markdownDisposeables.add(renderer.onDidRenderAsync(() => { - hoverContentsElement.className = 'hover-contents code-hover-contents'; - this._hover.onContentsChanged(); - })); - const renderedContents = markdownDisposeables.add(renderer.render(contents)); - hoverContentsElement.appendChild(renderedContents.element); - fragment.appendChild(markdownHoverElement); - isEmptyHoverContent = false; - }); + if (msg instanceof MarkdownHover) { + markdownParts.push(msg); + } } } }); - if (markerMessages.length) { - markerMessages.forEach(msg => fragment.appendChild(this.renderMarkerHover(msg))); - const markerHoverForStatusbar = markerMessages.length === 1 ? markerMessages[0] : markerMessages.sort((a, b) => MarkerSeverity.compare(a.marker.severity, b.marker.severity))[0]; - fragment.appendChild(this.renderMarkerStatusbar(markerHoverForStatusbar)); + if (markdownParts.length > 0) { + disposables.add(this._markdownHoverParticipant.renderHoverParts(markdownParts, fragment)); } + if (markerMessages.length) { + disposables.add(this._markerHoverParticipant.renderHoverParts(markerMessages, fragment)); + } + + this._renderDisposable = disposables; + // show - if (!containColorPicker && !isEmptyHoverContent) { + if (!containColorPicker && fragment.hasChildNodes()) { this.showAt(new Position(renderRange.startLineNumber, renderColumn), highlightRange, this._shouldFocus); - this.updateContents(fragment); + this._updateContents(fragment); } this._isChangingDecorations = true; @@ -514,175 +608,17 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._isChangingDecorations = false; } - private renderMarkerHover(markerHover: MarkerHover): HTMLElement { - const hoverElement = $('div.hover-row'); - const markerElement = dom.append(hoverElement, $('div.marker.hover-contents')); - const { source, message, code, relatedInformation } = markerHover.marker; - - this._editor.applyFontInfo(markerElement); - const messageElement = dom.append(markerElement, $('span')); - messageElement.style.whiteSpace = 'pre-wrap'; - messageElement.innerText = message; - - if (source || code) { - // Code has link - if (code && typeof code !== 'string') { - const sourceAndCodeElement = $('span'); - if (source) { - const sourceElement = dom.append(sourceAndCodeElement, $('span')); - sourceElement.innerText = source; - } - this._codeLink = dom.append(sourceAndCodeElement, $('a.code-link')); - this._codeLink.setAttribute('href', code.target.toString()); - - this._codeLink.onclick = (e) => { - this._openerService.open(code.target); - e.preventDefault(); - e.stopPropagation(); - }; - - const codeElement = dom.append(this._codeLink, $('span')); - codeElement.innerText = code.value; - - const detailsElement = dom.append(markerElement, sourceAndCodeElement); - detailsElement.style.opacity = '0.6'; - detailsElement.style.paddingLeft = '6px'; - } else { - const detailsElement = dom.append(markerElement, $('span')); - detailsElement.style.opacity = '0.6'; - detailsElement.style.paddingLeft = '6px'; - detailsElement.innerText = source && code ? `${source}(${code})` : source ? source : `(${code})`; - } - } - - if (isNonEmptyArray(relatedInformation)) { - for (const { message, resource, startLineNumber, startColumn } of relatedInformation) { - const relatedInfoContainer = dom.append(markerElement, $('div')); - relatedInfoContainer.style.marginTop = '8px'; - const a = dom.append(relatedInfoContainer, $('a')); - a.innerText = `${basename(resource)}(${startLineNumber}, ${startColumn}): `; - a.style.cursor = 'pointer'; - a.onclick = e => { - e.stopPropagation(); - e.preventDefault(); - if (this._openerService) { - this._openerService.open(resource.with({ fragment: `${startLineNumber},${startColumn}` }), { fromUserGesture: true }).catch(onUnexpectedError); - } - }; - const messageElement = dom.append(relatedInfoContainer, $('span')); - messageElement.innerText = message; - this._editor.applyFontInfo(messageElement); - } - } - - return hoverElement; - } - - private recentMarkerCodeActionsInfo: { marker: IMarker, hasCodeActions: boolean } | undefined = undefined; - private renderMarkerStatusbar(markerHover: MarkerHover): HTMLElement { - const hoverElement = $('div.hover-row.status-bar'); - const disposables = new DisposableStore(); - const actionsElement = dom.append(hoverElement, $('div.actions')); - if (markerHover.marker.severity === MarkerSeverity.Error || markerHover.marker.severity === MarkerSeverity.Warning || markerHover.marker.severity === MarkerSeverity.Info) { - disposables.add(this._renderAction(actionsElement, { - label: nls.localize('peek problem', "Peek Problem"), - commandId: NextMarkerAction.ID, - run: () => { - this.hide(); - MarkerController.get(this._editor).showAtMarker(markerHover.marker); - this._editor.focus(); - } - })); - } - - if (!this._editor.getOption(EditorOption.readOnly)) { - const quickfixPlaceholderElement = dom.append(actionsElement, $('div')); - if (this.recentMarkerCodeActionsInfo) { - if (IMarkerData.makeKey(this.recentMarkerCodeActionsInfo.marker) === IMarkerData.makeKey(markerHover.marker)) { - if (!this.recentMarkerCodeActionsInfo.hasCodeActions) { - quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available"); - } - } else { - this.recentMarkerCodeActionsInfo = undefined; - } - } - const updatePlaceholderDisposable = disposables.add(disposableTimeout(() => quickfixPlaceholderElement.textContent = nls.localize('checkingForQuickFixes', "Checking for quick fixes..."), 64)); - const codeActionsPromise = this.getCodeActions(markerHover.marker); - disposables.add(toDisposable(() => codeActionsPromise.cancel())); - codeActionsPromise.then(actions => { - updatePlaceholderDisposable.dispose(); - this.recentMarkerCodeActionsInfo = { marker: markerHover.marker, hasCodeActions: actions.validActions.length > 0 }; - - if (!this.recentMarkerCodeActionsInfo.hasCodeActions) { - actions.dispose(); - quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available"); - return; - } - quickfixPlaceholderElement.style.display = 'none'; - - let showing = false; - disposables.add(toDisposable(() => { - if (!showing) { - actions.dispose(); - } - })); - - disposables.add(this._renderAction(actionsElement, { - label: nls.localize('quick fixes', "Quick Fix..."), - commandId: QuickFixAction.Id, - run: (target) => { - showing = true; - const controller = QuickFixController.get(this._editor); - const elementPosition = dom.getDomNodePagePosition(target); - // Hide the hover pre-emptively, otherwise the editor can close the code actions - // context menu as well when using keyboard navigation - this.hide(); - controller.showCodeActions(markerCodeActionTrigger, actions, { - x: elementPosition.left + 6, - y: elementPosition.top + elementPosition.height + 6 - }); - } - })); - }); - } - - this.renderDisposable.value = disposables; - return hoverElement; - } - - private getCodeActions(marker: IMarker): CancelablePromise { - return createCancelablePromise(cancellationToken => { - return getCodeActions( - this._editor.getModel()!, - new Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn), - markerCodeActionTrigger, - Progress.None, - cancellationToken); - }); - } - private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ className: 'hoverHighlight' }); } -function hoverContentsEquals(first: HoverPart[], second: HoverPart[]): boolean { - if ((!first && second) || (first && !second) || first.length !== second.length) { +function hoverContentsEquals(first: HoverPartInfo[], second: HoverPartInfo[]): boolean { + if (first.length !== second.length) { return false; } for (let i = 0; i < first.length; i++) { - const firstElement = first[i]; - const secondElement = second[i]; - if (firstElement instanceof MarkerHover && secondElement instanceof MarkerHover) { - return IMarkerData.makeKey(firstElement.marker) === IMarkerData.makeKey(secondElement.marker); - } - if (firstElement instanceof ColorHover || secondElement instanceof ColorHover) { - return false; - } - if (firstElement instanceof MarkerHover || secondElement instanceof MarkerHover) { - return false; - } - if (!markedStringsEquals(firstElement.contents, secondElement.contents)) { + if (!first[i].data.equals(second[i].data)) { return false; } } diff --git a/src/vs/editor/contrib/inlineHints/inlineHintsController.ts b/src/vs/editor/contrib/inlineHints/inlineHintsController.ts new file mode 100644 index 000000000..43750f989 --- /dev/null +++ b/src/vs/editor/contrib/inlineHints/inlineHintsController.ts @@ -0,0 +1,227 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from 'vs/base/common/async'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { hash } from 'vs/base/common/hash'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IContentDecorationRenderOptions, IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { InlineHintsProvider, InlineHintsProviderRegistry, InlineHint } from 'vs/editor/common/modes'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { flatten } from 'vs/base/common/arrays'; +import { editorInlineHintForeground, editorInlineHintBackground } from 'vs/platform/theme/common/colorRegistry'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { Range } from 'vs/editor/common/core/range'; +import { LanguageFeatureRequestDelays } from 'vs/editor/common/modes/languageFeatureRegistry'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { URI } from 'vs/base/common/uri'; +import { IRange } from 'vs/base/common/range'; +import { assertType } from 'vs/base/common/types'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; + +const MAX_DECORATORS = 500; + +export interface InlineHintsData { + list: InlineHint[]; + provider: InlineHintsProvider; +} + +export async function getInlineHints(model: ITextModel, ranges: Range[], token: CancellationToken): Promise { + const datas: InlineHintsData[] = []; + const providers = InlineHintsProviderRegistry.ordered(model).reverse(); + const promises = flatten(providers.map(provider => ranges.map(range => Promise.resolve(provider.provideInlineHints(model, range, token)).then(result => { + if (result) { + datas.push({ list: result, provider }); + } + }, err => { + onUnexpectedExternalError(err); + })))); + + await Promise.all(promises); + + return datas; +} + +export class InlineHintsController implements IEditorContribution { + + static readonly ID: string = 'editor.contrib.InlineHints'; + + // static get(editor: ICodeEditor): InlineHintsController { + // return editor.getContribution(this.ID); + // } + + private readonly _disposables = new DisposableStore(); + private readonly _sessionDisposables = new DisposableStore(); + private readonly _getInlineHintsDelays = new LanguageFeatureRequestDelays(InlineHintsProviderRegistry, 250, 2500); + + private _decorationsTypeIds: string[] = []; + private _decorationIds: string[] = []; + + constructor( + private readonly _editor: ICodeEditor, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IThemeService private readonly _themeService: IThemeService, + ) { + this._disposables.add(InlineHintsProviderRegistry.onDidChange(() => this._update())); + this._disposables.add(_themeService.onDidColorThemeChange(() => this._update())); + this._disposables.add(_editor.onDidChangeModel(() => this._update())); + this._disposables.add(_editor.onDidChangeModelLanguage(() => this._update())); + this._disposables.add(_editor.onDidChangeConfiguration(e => { + if (e.hasChanged(EditorOption.inlineHints)) { + this._update(); + } + })); + + this._update(); + } + + dispose(): void { + this._sessionDisposables.dispose(); + this._removeAllDecorations(); + this._disposables.dispose(); + } + + private _update(): void { + this._sessionDisposables.clear(); + + if (!this._editor.getOption(EditorOption.inlineHints).enabled) { + this._removeAllDecorations(); + return; + } + + const model = this._editor.getModel(); + if (!model || !InlineHintsProviderRegistry.has(model)) { + this._removeAllDecorations(); + return; + } + + const scheduler = new RunOnceScheduler(async () => { + const t1 = Date.now(); + + const cts = new CancellationTokenSource(); + this._sessionDisposables.add(toDisposable(() => cts.dispose(true))); + + const visibleRanges = this._editor.getVisibleRangesPlusViewportAboveBelow(); + const result = await getInlineHints(model, visibleRanges, cts.token); + + // update moving average + const newDelay = this._getInlineHintsDelays.update(model, Date.now() - t1); + scheduler.delay = newDelay; + + // render hints + this._updateHintsDecorators(result); + + }, this._getInlineHintsDelays.get(model)); + + this._sessionDisposables.add(scheduler); + + // update inline hints when content or scroll position changes + this._sessionDisposables.add(this._editor.onDidChangeModelContent(() => scheduler.schedule())); + this._disposables.add(this._editor.onDidScrollChange(() => scheduler.schedule())); + scheduler.schedule(); + + // update inline hints when any any provider fires an event + const providerListener = new DisposableStore(); + this._sessionDisposables.add(providerListener); + for (const provider of InlineHintsProviderRegistry.all(model)) { + if (typeof provider.onDidChangeInlineHints === 'function') { + providerListener.add(provider.onDidChangeInlineHints(() => scheduler.schedule())); + } + } + } + + private _updateHintsDecorators(hintsData: InlineHintsData[]): void { + const { fontSize, fontFamily } = this._getLayoutInfo(); + const backgroundColor = this._themeService.getColorTheme().getColor(editorInlineHintBackground); + const fontColor = this._themeService.getColorTheme().getColor(editorInlineHintForeground); + + const newDecorationsTypeIds: string[] = []; + const newDecorationsData: IModelDeltaDecoration[] = []; + + for (const { list: hints } of hintsData) { + + for (let j = 0; j < hints.length && newDecorationsData.length < MAX_DECORATORS; j++) { + const { text, range, description: hoverMessage, whitespaceBefore, whitespaceAfter } = hints[j]; + const marginBefore = whitespaceBefore ? (fontSize / 3) | 0 : 0; + const marginAfter = whitespaceAfter ? (fontSize / 3) | 0 : 0; + + const before: IContentDecorationRenderOptions = { + contentText: text, + backgroundColor: `${backgroundColor}`, + color: `${fontColor}`, + margin: `0px ${marginAfter}px 0px ${marginBefore}px`, + fontSize: `${fontSize}px`, + fontFamily: fontFamily, + padding: `0px ${(fontSize / 4) | 0}px`, + borderRadius: `${(fontSize / 4) | 0}px`, + }; + const key = 'inlineHints-' + hash(before).toString(16); + this._codeEditorService.registerDecorationType(key, { before }, undefined, this._editor); + + // decoration types are ref-counted which means we only need to + // call register und remove equally often + newDecorationsTypeIds.push(key); + + const options = this._codeEditorService.resolveDecorationOptions(key, true); + if (typeof hoverMessage === 'string') { + options.hoverMessage = new MarkdownString().appendText(hoverMessage); + } else if (hoverMessage) { + options.hoverMessage = hoverMessage; + } + + newDecorationsData.push({ + range, + options + }); + } + } + + this._decorationsTypeIds.forEach(this._codeEditorService.removeDecorationType, this._codeEditorService); + this._decorationsTypeIds = newDecorationsTypeIds; + + this._decorationIds = this._editor.deltaDecorations(this._decorationIds, newDecorationsData); + } + + private _getLayoutInfo() { + const options = this._editor.getOption(EditorOption.inlineHints); + const editorFontSize = this._editor.getOption(EditorOption.fontSize); + let fontSize = options.fontSize; + if (!fontSize || fontSize < 5 || fontSize > editorFontSize) { + fontSize = (editorFontSize * .9) | 0; + } + const fontFamily = options.fontFamily; + return { fontSize, fontFamily }; + } + + private _removeAllDecorations(): void { + this._decorationIds = this._editor.deltaDecorations(this._decorationIds, []); + this._decorationsTypeIds.forEach(this._codeEditorService.removeDecorationType, this._codeEditorService); + this._decorationsTypeIds = []; + } +} + +registerEditorContribution(InlineHintsController.ID, InlineHintsController); + +CommandsRegistry.registerCommand('_executeInlineHintProvider', async (accessor, ...args: [URI, IRange]): Promise => { + + const [uri, range] = args; + assertType(URI.isUri(uri)); + assertType(Range.isIRange(range)); + + const ref = await accessor.get(ITextModelService).createModelReference(uri); + try { + const data = await getInlineHints(ref.object.textEditorModel, [Range.lift(range)], CancellationToken.None); + return flatten(data.map(item => item.list)).sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + + } finally { + ref.dispose(); + } +}); diff --git a/src/vs/editor/contrib/linesOperations/linesOperations.ts b/src/vs/editor/contrib/linesOperations/linesOperations.ts index 9c365d507..54fcba6ac 100644 --- a/src/vs/editor/contrib/linesOperations/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/linesOperations.ts @@ -939,43 +939,39 @@ export class TransposeAction extends EditorAction { export abstract class AbstractCaseAction extends EditorAction { public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { - let selections = editor.getSelections(); + const selections = editor.getSelections(); if (selections === null) { return; } - let model = editor.getModel(); + const model = editor.getModel(); if (model === null) { return; } - let wordSeparators = editor.getOption(EditorOption.wordSeparators); + const wordSeparators = editor.getOption(EditorOption.wordSeparators); + const textEdits: IIdentifiedSingleEditOperation[] = []; - let commands: ICommand[] = []; - - for (let i = 0, len = selections.length; i < len; i++) { - let selection = selections[i]; + for (const selection of selections) { if (selection.isEmpty()) { - let cursor = selection.getStartPosition(); + const cursor = selection.getStartPosition(); const word = editor.getConfiguredWordAtPosition(cursor); if (!word) { continue; } - let wordRange = new Range(cursor.lineNumber, word.startColumn, cursor.lineNumber, word.endColumn); - let text = model.getValueInRange(wordRange); - commands.push(new ReplaceCommandThatPreservesSelection(wordRange, this._modifyText(text, wordSeparators), - new Selection(cursor.lineNumber, cursor.column, cursor.lineNumber, cursor.column))); - + const wordRange = new Range(cursor.lineNumber, word.startColumn, cursor.lineNumber, word.endColumn); + const text = model.getValueInRange(wordRange); + textEdits.push(EditOperation.replace(wordRange, this._modifyText(text, wordSeparators))); } else { - let text = model.getValueInRange(selection); - commands.push(new ReplaceCommandThatPreservesSelection(selection, this._modifyText(text, wordSeparators), selection)); + const text = model.getValueInRange(selection); + textEdits.push(EditOperation.replace(selection, this._modifyText(text, wordSeparators))); } } editor.pushUndoStop(); - editor.executeCommands(this.id, commands); + editor.executeEdits(this.id, textEdits); editor.pushUndoStop(); } @@ -1049,6 +1045,25 @@ export class TitleCaseAction extends AbstractCaseAction { } } +export class SnakeCaseAction extends AbstractCaseAction { + constructor() { + super({ + id: 'editor.action.transformToSnakecase', + label: nls.localize('editor.transformToSnakecase', "Transform to Snake Case"), + alias: 'Transform to Snake Case', + precondition: EditorContextKeys.writable + }); + } + + protected _modifyText(text: string, wordSeparators: string): string { + return (text + .replace(/(\p{Ll})(\p{Lu})/gmu, '$1_$2') + .replace(/([^\b_])(\p{Lu})(\p{Ll})/gmu, '$1_$2$3') + .toLocaleLowerCase() + ); + } +} + registerEditorAction(CopyLinesUpAction); registerEditorAction(CopyLinesDownAction); registerEditorAction(DuplicateSelectionAction); @@ -1069,3 +1084,4 @@ registerEditorAction(TransposeAction); registerEditorAction(UpperCaseAction); registerEditorAction(LowerCaseAction); registerEditorAction(TitleCaseAction); +registerEditorAction(SnakeCaseAction); diff --git a/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts index 681c8f027..6cdc4a4d0 100644 --- a/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts @@ -299,13 +299,13 @@ export class MoveLinesCommand implements ICommand { } } - private matchEnterRule(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, oneLineAbove: number, oneLineAboveText?: string) { + private matchEnterRule(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, oneLineAbove: number, previousLineText?: string) { let validPrecedingLine = oneLineAbove; while (validPrecedingLine >= 1) { // ship empty lines as empty lines just inherit indentation let lineContent; - if (validPrecedingLine === oneLineAbove && oneLineAboveText !== undefined) { - lineContent = oneLineAboveText; + if (validPrecedingLine === oneLineAbove && previousLineText !== undefined) { + lineContent = previousLineText; } else { lineContent = model.getLineContent(validPrecedingLine); } diff --git a/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts index 6777693d4..64f3c7a5d 100644 --- a/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts @@ -207,8 +207,8 @@ suite('Editor Contrib - Duplicate Selection', () => { withTestCodeEditor(lines.join('\n'), {}, (editor) => { editor.setSelections(selections); duplicateSelectionAction.run(null!, editor, {}); - assert.deepEqual(editor.getValue(), expectedLines.join('\n')); - assert.deepEqual(editor.getSelections()!.map(s => s.toString()), expectedSelections.map(s => s.toString())); + assert.deepStrictEqual(editor.getValue(), expectedLines.join('\n')); + assert.deepStrictEqual(editor.getSelections()!.map(s => s.toString()), expectedSelections.map(s => s.toString())); }); } diff --git a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts index cdcb7f491..b3cbe4bdc 100644 --- a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts @@ -8,7 +8,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { Handler } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { TitleCaseAction, DeleteAllLeftAction, DeleteAllRightAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, LowerCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TransposeAction, UpperCaseAction, DeleteLinesAction } from 'vs/editor/contrib/linesOperations/linesOperations'; +import { TitleCaseAction, DeleteAllLeftAction, DeleteAllRightAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, LowerCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TransposeAction, UpperCaseAction, DeleteLinesAction, SnakeCaseAction } from 'vs/editor/contrib/linesOperations/linesOperations'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -19,7 +19,7 @@ function assertSelection(editor: ICodeEditor, expected: Selection | Selection[]) if (!Array.isArray(expected)) { expected = [expected]; } - assert.deepEqual(editor.getSelections(), expected); + assert.deepStrictEqual(editor.getSelections(), expected); } function executeAction(action: EditorAction, editor: ICodeEditor): void { @@ -40,7 +40,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 3, 5)); executeAction(sortLinesAscendingAction, editor); - assert.deepEqual(model.getLinesContent(), [ + assert.deepStrictEqual(model.getLinesContent(), [ 'alpha', 'beta', 'omicron' @@ -65,7 +65,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelections([new Selection(1, 1, 3, 5), new Selection(5, 1, 7, 5)]); executeAction(sortLinesAscendingAction, editor); - assert.deepEqual(model.getLinesContent(), [ + assert.deepStrictEqual(model.getLinesContent(), [ 'alpha', 'beta', 'omicron', @@ -79,7 +79,7 @@ suite('Editor Contrib - Line Operations', () => { new Selection(5, 1, 7, 7) ]; editor.getSelections()!.forEach((actualSelection, index) => { - assert.deepEqual(actualSelection.toString(), expectedSelections[index].toString()); + assert.deepStrictEqual(actualSelection.toString(), expectedSelections[index].toString()); }); }); }); @@ -98,7 +98,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 3, 7)); executeAction(sortLinesDescendingAction, editor); - assert.deepEqual(model.getLinesContent(), [ + assert.deepStrictEqual(model.getLinesContent(), [ 'omicron', 'beta', 'alpha' @@ -123,7 +123,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelections([new Selection(1, 1, 3, 7), new Selection(5, 1, 7, 7)]); executeAction(sortLinesDescendingAction, editor); - assert.deepEqual(model.getLinesContent(), [ + assert.deepStrictEqual(model.getLinesContent(), [ 'omicron', 'beta', 'alpha', @@ -137,7 +137,7 @@ suite('Editor Contrib - Line Operations', () => { new Selection(5, 1, 7, 5) ]; editor.getSelections()!.forEach((actualSelection, index) => { - assert.deepEqual(actualSelection.toString(), expectedSelections[index].toString()); + assert.deepStrictEqual(actualSelection.toString(), expectedSelections[index].toString()); }); }); }); @@ -157,12 +157,12 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 2, 1, 2)); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(1), 'ne'); + assert.strictEqual(model.getLineContent(1), 'ne'); editor.setSelections([new Selection(2, 2, 2, 2), new Selection(3, 2, 3, 2)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(2), 'wo'); - assert.equal(model.getLineContent(3), 'hree'); + assert.strictEqual(model.getLineContent(2), 'wo'); + assert.strictEqual(model.getLineContent(3), 'hree'); }); }); @@ -178,16 +178,16 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(2, 1, 2, 1)); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(1), 'onetwo'); + assert.strictEqual(model.getLineContent(1), 'onetwo'); editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLinesContent()[0], 'onetwothree'); - assert.equal(model.getLinesContent().length, 1); + assert.strictEqual(model.getLinesContent()[0], 'onetwothree'); + assert.strictEqual(model.getLinesContent().length, 1); editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLinesContent()[0], 'onetwothree'); + assert.strictEqual(model.getLinesContent()[0], 'onetwothree'); }); }); @@ -213,25 +213,25 @@ suite('Editor Contrib - Line Operations', () => { executeAction(deleteAllLeftAction, editor); let selections = editor.getSelections()!; - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ' waso waso'); - assert.equal(model.getLineContent(5), ''); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ' waso waso'); + assert.strictEqual(model.getLineContent(5), ''); - assert.deepEqual([ + assert.deepStrictEqual([ selections[0].startLineNumber, selections[0].startColumn, selections[0].endLineNumber, selections[0].endColumn ], [3, 1, 3, 1]); - assert.deepEqual([ + assert.deepStrictEqual([ selections[1].startLineNumber, selections[1].startColumn, selections[1].endLineNumber, selections[1].endColumn ], [2, 1, 2, 1]); - assert.deepEqual([ + assert.deepStrictEqual([ selections[2].startLineNumber, selections[2].startColumn, selections[2].endLineNumber, @@ -241,17 +241,17 @@ suite('Editor Contrib - Line Operations', () => { executeAction(deleteAllLeftAction, editor); selections = editor.getSelections()!; - assert.equal(model.getLineContent(1), 'hi my name is Carlos Matos waso waso'); - assert.equal(selections.length, 2); + assert.strictEqual(model.getLineContent(1), 'hi my name is Carlos Matos waso waso'); + assert.strictEqual(selections.length, 2); - assert.deepEqual([ + assert.deepStrictEqual([ selections[0].startLineNumber, selections[0].startColumn, selections[0].endLineNumber, selections[0].endColumn ], [1, 27, 1, 27]); - assert.deepEqual([ + assert.deepStrictEqual([ selections[1].startLineNumber, selections[1].startColumn, selections[1].endLineNumber, @@ -277,23 +277,23 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelections([new Selection(1, 2, 1, 2), new Selection(1, 4, 1, 4)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(1), 'lo'); + assert.strictEqual(model.getLineContent(1), 'lo'); editor.setSelections([new Selection(2, 2, 2, 2), new Selection(2, 4, 2, 5)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(2), 'd'); + assert.strictEqual(model.getLineContent(2), 'd'); editor.setSelections([new Selection(3, 2, 3, 5), new Selection(3, 7, 3, 7)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(3), 'world'); + assert.strictEqual(model.getLineContent(3), 'world'); editor.setSelections([new Selection(4, 3, 4, 3), new Selection(4, 5, 5, 4)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(4), 'jour'); + assert.strictEqual(model.getLineContent(4), 'jour'); editor.setSelections([new Selection(5, 3, 6, 3), new Selection(6, 5, 7, 5), new Selection(7, 7, 7, 7)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(5), 'world'); + assert.strictEqual(model.getLineContent(5), 'world'); }); }); @@ -310,16 +310,16 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 1)); editor.trigger('keyboard', Handler.Type, { text: 'Typing some text here on line ' }); - assert.equal(model.getLineContent(1), 'Typing some text here on line one'); - assert.deepEqual(editor.getSelection(), new Selection(1, 31, 1, 31)); + assert.strictEqual(model.getLineContent(1), 'Typing some text here on line one'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 31, 1, 31)); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(1), 'one'); - assert.deepEqual(editor.getSelection(), new Selection(1, 1, 1, 1)); + assert.strictEqual(model.getLineContent(1), 'one'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 1, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Typing some text here on line one'); - assert.deepEqual(editor.getSelection(), new Selection(1, 31, 1, 31)); + assert.strictEqual(model.getLineContent(1), 'Typing some text here on line one'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 31, 1, 31)); }); }); }); @@ -345,27 +345,27 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 2, 1, 2)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(1), 'hello world'); + assert.strictEqual(model.getLineContent(1), 'hello world'); assertSelection(editor, new Selection(1, 6, 1, 6)); editor.setSelection(new Selection(2, 2, 2, 2)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(2), 'hello world'); + assert.strictEqual(model.getLineContent(2), 'hello world'); assertSelection(editor, new Selection(2, 7, 2, 7)); editor.setSelection(new Selection(3, 2, 3, 2)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(3), 'hello world'); + assert.strictEqual(model.getLineContent(3), 'hello world'); assertSelection(editor, new Selection(3, 7, 3, 7)); editor.setSelection(new Selection(4, 2, 5, 3)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(4), 'hello world'); + assert.strictEqual(model.getLineContent(4), 'hello world'); assertSelection(editor, new Selection(4, 2, 4, 8)); editor.setSelection(new Selection(5, 1, 7, 3)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(5), 'hello world'); + assert.strictEqual(model.getLineContent(5), 'hello world'); assertSelection(editor, new Selection(5, 1, 5, 3)); }); }); @@ -381,8 +381,8 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(2, 1, 2, 1)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(1), 'hello'); - assert.equal(model.getLineContent(2), 'world'); + assert.strictEqual(model.getLineContent(1), 'hello'); + assert.strictEqual(model.getLineContent(2), 'world'); assertSelection(editor, new Selection(2, 6, 2, 6)); }); }); @@ -416,7 +416,7 @@ suite('Editor Contrib - Line Operations', () => { ]); executeAction(joinLinesAction, editor); - assert.equal(model.getLinesContent().join('\n'), 'hello world\nhello world\nhello world\nhello world\n\nhello world'); + assert.strictEqual(model.getLinesContent().join('\n'), 'hello world\nhello world\nhello world\nhello world\n\nhello world'); assertSelection(editor, [ /** primary cursor */ new Selection(3, 4, 3, 8), @@ -440,16 +440,16 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 6, 1, 6)); editor.trigger('keyboard', Handler.Type, { text: ' my dear' }); - assert.equal(model.getLineContent(1), 'hello my dear'); - assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); + assert.strictEqual(model.getLineContent(1), 'hello my dear'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(1), 'hello my dear world'); - assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); + assert.strictEqual(model.getLineContent(1), 'hello my dear world'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'hello my dear'); - assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); + assert.strictEqual(model.getLineContent(1), 'hello my dear'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); }); }); }); @@ -467,27 +467,27 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(1), 'hello world'); + assert.strictEqual(model.getLineContent(1), 'hello world'); assertSelection(editor, new Selection(1, 2, 1, 2)); editor.setSelection(new Selection(1, 6, 1, 6)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(1), 'hell oworld'); + assert.strictEqual(model.getLineContent(1), 'hell oworld'); assertSelection(editor, new Selection(1, 7, 1, 7)); editor.setSelection(new Selection(1, 12, 1, 12)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(1), 'hell oworl'); + assert.strictEqual(model.getLineContent(1), 'hell oworl'); assertSelection(editor, new Selection(2, 2, 2, 2)); editor.setSelection(new Selection(3, 1, 3, 1)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(3), ''); assertSelection(editor, new Selection(4, 1, 4, 1)); editor.setSelection(new Selection(4, 2, 4, 2)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(4), ' '); + assert.strictEqual(model.getLineContent(4), ' '); assertSelection(editor, new Selection(4, 3, 4, 3)); } ); @@ -509,22 +509,22 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(2), ''); assertSelection(editor, new Selection(2, 1, 2, 1)); editor.setSelection(new Selection(3, 6, 3, 6)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(4), 'oworld'); + assert.strictEqual(model.getLineContent(4), 'oworld'); assertSelection(editor, new Selection(4, 2, 4, 2)); editor.setSelection(new Selection(6, 12, 6, 12)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(7), 'd'); + assert.strictEqual(model.getLineContent(7), 'd'); assertSelection(editor, new Selection(7, 2, 7, 2)); editor.setSelection(new Selection(8, 12, 8, 12)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(8), 'hello world'); + assert.strictEqual(model.getLineContent(8), 'hello world'); assertSelection(editor, new Selection(8, 12, 8, 12)); } ); @@ -534,52 +534,131 @@ suite('Editor Contrib - Line Operations', () => { withTestCodeEditor( [ 'hello world', - 'öçşğü' + 'öçşğü', + 'parseHTMLString', + 'getElementById', + 'insertHTML', + 'PascalCase', + 'CSSSelectorsList', + 'iD', + 'tEST', + 'öçşÖÇŞğüĞÜ', + 'audioConverter.convertM4AToMP3();', + 'snake_case', + 'Capital_Snake_Case', + `function helloWorld() { + return someGlobalObject.printHelloWorld("en", "utf-8"); + } + helloWorld();`.replace(/^\s+/gm, '') ], {}, (editor) => { let model = editor.getModel()!; let uppercaseAction = new UpperCaseAction(); let lowercaseAction = new LowerCaseAction(); let titlecaseAction = new TitleCaseAction(); + let snakecaseAction = new SnakeCaseAction(); editor.setSelection(new Selection(1, 1, 1, 12)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(1), 'HELLO WORLD'); + assert.strictEqual(model.getLineContent(1), 'HELLO WORLD'); assertSelection(editor, new Selection(1, 1, 1, 12)); editor.setSelection(new Selection(1, 1, 1, 12)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(1), 'hello world'); + assert.strictEqual(model.getLineContent(1), 'hello world'); assertSelection(editor, new Selection(1, 1, 1, 12)); editor.setSelection(new Selection(1, 3, 1, 3)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(1), 'HELLO world'); + assert.strictEqual(model.getLineContent(1), 'HELLO world'); assertSelection(editor, new Selection(1, 3, 1, 3)); editor.setSelection(new Selection(1, 4, 1, 4)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(1), 'hello world'); + assert.strictEqual(model.getLineContent(1), 'hello world'); assertSelection(editor, new Selection(1, 4, 1, 4)); editor.setSelection(new Selection(1, 1, 1, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(1), 'Hello World'); + assert.strictEqual(model.getLineContent(1), 'Hello World'); assertSelection(editor, new Selection(1, 1, 1, 12)); editor.setSelection(new Selection(2, 1, 2, 6)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(2), 'ÖÇŞĞÜ'); + assert.strictEqual(model.getLineContent(2), 'ÖÇŞĞÜ'); assertSelection(editor, new Selection(2, 1, 2, 6)); editor.setSelection(new Selection(2, 1, 2, 6)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(2), 'öçşğü'); + assert.strictEqual(model.getLineContent(2), 'öçşğü'); assertSelection(editor, new Selection(2, 1, 2, 6)); editor.setSelection(new Selection(2, 1, 2, 6)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(2), 'Öçşğü'); + assert.strictEqual(model.getLineContent(2), 'Öçşğü'); assertSelection(editor, new Selection(2, 1, 2, 6)); + + editor.setSelection(new Selection(3, 1, 3, 16)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(3), 'parse_html_string'); + assertSelection(editor, new Selection(3, 1, 3, 18)); + + editor.setSelection(new Selection(4, 1, 4, 15)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(4), 'get_element_by_id'); + assertSelection(editor, new Selection(4, 1, 4, 18)); + + editor.setSelection(new Selection(5, 1, 5, 11)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(5), 'insert_html'); + assertSelection(editor, new Selection(5, 1, 5, 12)); + + editor.setSelection(new Selection(6, 1, 6, 11)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(6), 'pascal_case'); + assertSelection(editor, new Selection(6, 1, 6, 12)); + + editor.setSelection(new Selection(7, 1, 7, 17)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(7), 'css_selectors_list'); + assertSelection(editor, new Selection(7, 1, 7, 19)); + + editor.setSelection(new Selection(8, 1, 8, 3)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(8), 'i_d'); + assertSelection(editor, new Selection(8, 1, 8, 4)); + + editor.setSelection(new Selection(9, 1, 9, 5)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(9), 't_est'); + assertSelection(editor, new Selection(9, 1, 9, 6)); + + editor.setSelection(new Selection(10, 1, 10, 11)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(10), 'öçş_öç_şğü_ğü'); + assertSelection(editor, new Selection(10, 1, 10, 14)); + + editor.setSelection(new Selection(11, 1, 11, 34)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(11), 'audio_converter.convert_m4a_to_mp3();'); + assertSelection(editor, new Selection(11, 1, 11, 38)); + + editor.setSelection(new Selection(12, 1, 12, 11)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(12), 'snake_case'); + assertSelection(editor, new Selection(12, 1, 12, 11)); + + editor.setSelection(new Selection(13, 1, 13, 19)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(13), 'capital_snake_case'); + assertSelection(editor, new Selection(13, 1, 13, 19)); + + editor.setSelection(new Selection(14, 1, 17, 14)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getValueInRange(new Selection(14, 1, 17, 15)), `function hello_world() { + return some_global_object.print_hello_world("en", "utf-8"); + } + hello_world();`.replace(/^\s+/gm, '')); + assertSelection(editor, new Selection(14, 1, 17, 15)); } ); @@ -597,27 +676,27 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(1), 'Foo Bar Baz'); + assert.strictEqual(model.getLineContent(1), 'Foo Bar Baz'); editor.setSelection(new Selection(2, 1, 2, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(2), 'Foo\'Bar\'Baz'); + assert.strictEqual(model.getLineContent(2), 'Foo\'Bar\'Baz'); editor.setSelection(new Selection(3, 1, 3, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(3), 'Foo[Bar]Baz'); + assert.strictEqual(model.getLineContent(3), 'Foo[Bar]Baz'); editor.setSelection(new Selection(4, 1, 4, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(4), 'Foo`Bar~Baz'); + assert.strictEqual(model.getLineContent(4), 'Foo`Bar~Baz'); editor.setSelection(new Selection(5, 1, 5, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(5), 'Foo^Bar%Baz'); + assert.strictEqual(model.getLineContent(5), 'Foo^Bar%Baz'); editor.setSelection(new Selection(6, 1, 6, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(6), 'Foo$Bar!Baz'); + assert.strictEqual(model.getLineContent(6), 'Foo$Bar!Baz'); } ); @@ -632,22 +711,22 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(1), ''); assertSelection(editor, new Selection(1, 1, 1, 1)); editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(1), ''); assertSelection(editor, new Selection(1, 1, 1, 1)); editor.setSelection(new Selection(2, 2, 2, 2)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(2), ' '); assertSelection(editor, new Selection(2, 2, 2, 2)); editor.setSelection(new Selection(2, 2, 2, 2)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(2), ' '); assertSelection(editor, new Selection(2, 2, 2, 2)); } ); @@ -660,18 +739,18 @@ suite('Editor Contrib - Line Operations', () => { const action = new DeleteAllRightAction(); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); editor.setSelections([new Selection(1, 1, 1, 1), new Selection(1, 1, 1, 1), new Selection(1, 1, 1, 1)]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); }); }); @@ -685,18 +764,18 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 2, 1, 5)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['ho', 'world']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 2, 1, 2)]); + assert.deepStrictEqual(model.getLinesContent(), ['ho', 'world']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 2, 1, 2)]); editor.setSelection(new Selection(1, 1, 2, 4)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['ld']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['ld']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); editor.setSelection(new Selection(1, 1, 1, 3)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); }); }); @@ -710,13 +789,13 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 3, 1, 3)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he', 'world']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 3, 1, 3)]); + assert.deepStrictEqual(model.getLinesContent(), ['he', 'world']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 3, 1, 3)]); editor.setSelection(new Selection(2, 1, 2, 1)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he', '']); - assert.deepEqual(editor.getSelections(), [new Selection(2, 1, 2, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['he', '']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(2, 1, 2, 1)]); }); }); @@ -730,18 +809,18 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 6, 1, 6)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['helloworld']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(model.getLinesContent(), ['helloworld']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); editor.setSelection(new Selection(1, 6, 1, 6)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hello']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(model.getLinesContent(), ['hello']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); editor.setSelection(new Selection(1, 6, 1, 6)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hello']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(model.getLinesContent(), ['hello']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); }); }); @@ -760,35 +839,35 @@ suite('Editor Contrib - Line Operations', () => { new Selection(3, 4, 3, 4), ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hethere', 'wor']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['hethere', 'wor']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he', 'wor']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['he', 'wor']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hewor']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['hewor']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(1, 6, 1, 6) ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['he']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3) ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['he']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3) ]); }); @@ -809,20 +888,20 @@ suite('Editor Contrib - Line Operations', () => { new Selection(3, 4, 3, 4), ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hethere', 'wor']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['hethere', 'wor']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) ]); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(1, 6, 1, 6), new Selection(3, 4, 3, 4) ]); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) ]); @@ -847,27 +926,27 @@ suite('Editor Contrib - Line Operations', () => { } testInsertLineBefore(1, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(1, 1, 1, 1)); - assert.equal(model.getLineContent(1), ''); - assert.equal(model.getLineContent(2), 'First line'); - assert.equal(model.getLineContent(3), 'Second line'); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 1, 1, 1)); + assert.strictEqual(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(2), 'First line'); + assert.strictEqual(model.getLineContent(3), 'Second line'); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); testInsertLineBefore(2, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(2, 1, 2, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), 'Second line'); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(2, 1, 2, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), 'Second line'); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); testInsertLineBefore(3, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(3, 1, 3, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), 'Second line'); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(3, 1, 3, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), 'Second line'); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); }); @@ -888,27 +967,27 @@ suite('Editor Contrib - Line Operations', () => { } testInsertLineAfter(1, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(2, 1, 2, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), 'Second line'); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(2, 1, 2, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), 'Second line'); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); testInsertLineAfter(2, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(3, 1, 3, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), 'Second line'); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(3, 1, 3, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), 'Second line'); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); testInsertLineAfter(3, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(4, 1, 4, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), 'Second line'); - assert.equal(model.getLineContent(3), 'Third line'); - assert.equal(model.getLineContent(4), ''); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(4, 1, 4, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), 'Second line'); + assert.strictEqual(model.getLineContent(3), 'Third line'); + assert.strictEqual(model.getLineContent(4), ''); }); }); @@ -928,11 +1007,11 @@ suite('Editor Contrib - Line Operations', () => { editor.setPosition(new Position(1, 2)); executeAction(indentLinesAction, editor); - assert.equal(model.getLineContent(1), '\tfunction baz() {'); - assert.deepEqual(editor.getSelection(), new Selection(1, 3, 1, 3)); + assert.strictEqual(model.getLineContent(1), '\tfunction baz() {'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 3, 1, 3)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), '\tf\tunction baz() {'); + assert.strictEqual(model.getLineContent(1), '\tf\tunction baz() {'); }); model.dispose(); @@ -953,8 +1032,8 @@ suite('Editor Contrib - Line Operations', () => { editor.setPosition(new Position(1, 1)); executeAction(indentLinesAction, editor); - assert.equal(model.getLineContent(1), '\tSome text'); - assert.deepEqual(editor.getSelection(), new Selection(1, 2, 1, 2)); + assert.strictEqual(model.getLineContent(1), '\tSome text'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 2, 1, 2)); }); model.dispose(); @@ -972,8 +1051,8 @@ suite('Editor Contrib - Line Operations', () => { editor.setPosition(new Position(1, 1)); executeAction(indentLinesAction, editor); - assert.equal(model.getLineContent(1), ' '); - assert.deepEqual(editor.getSelection(), new Selection(1, 5, 1, 5)); + assert.strictEqual(model.getLineContent(1), ' '); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 5, 1, 5)); }); model.dispose(); @@ -995,7 +1074,7 @@ suite('Editor Contrib - Line Operations', () => { const deleteLinesAction = new DeleteLinesAction(); executeAction(deleteLinesAction, editor); - assert.equal(editor.getValue(), 'a\nc'); + assert.strictEqual(editor.getValue(), 'a\nc'); }); }); @@ -1007,8 +1086,8 @@ suite('Editor Contrib - Line Operations', () => { const deleteLinesAction = new DeleteLinesAction(); executeAction(deleteLinesAction, editor); - assert.equal(editor.getValue(), resultingText.join('\n')); - assert.deepEqual(editor.getSelections(), resultingSelections); + assert.strictEqual(editor.getValue(), resultingText.join('\n')); + assert.deepStrictEqual(editor.getSelections(), resultingSelections); }); } diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index 04e8b8aa0..8997154ce 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -57,7 +57,7 @@ function getHoverMessage(link: Link, useMetaKey: boolean): MarkdownString { nativeLabel = ` "${nativeLabelText}"`; } } - const hoverMessage = new MarkdownString('', true).appendMarkdown(`[${label}](${link.url.toString()}${nativeLabel}) (${kb})`); + const hoverMessage = new MarkdownString('', true).appendMarkdown(`[${label}](${link.url.toString(true)}${nativeLabel}) (${kb})`); return hoverMessage; } else { return new MarkdownString().appendText(`${label} (${kb})`); @@ -327,7 +327,7 @@ export class LinkDetector implements IEditorContribution { } } - return this.openerService.open(uri, { openToSide, fromUserGesture }); + return this.openerService.open(uri, { openToSide, fromUserGesture, allowContributedOpeners: true }); }, err => { const messageOrError = diff --git a/src/vs/editor/contrib/multicursor/multicursor.ts b/src/vs/editor/contrib/multicursor/multicursor.ts index 3606348fb..599948853 100644 --- a/src/vs/editor/contrib/multicursor/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/multicursor.ts @@ -850,7 +850,6 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut this.updateSoon.schedule(); } else { this._setState(null); - } } else { this._update(); @@ -1016,11 +1015,6 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut }); this.decorations = this.editor.deltaDecorations(this.decorations, decorations); - - const currentFindState = CommonFindController.get(this.editor).getState(); - if (currentFindState.isRegex || currentFindState.matchCase || currentFindState.wholeWord) { - CommonFindController.get(this.editor).highlightFindOptions(true); - } } private static readonly _SELECTION_HIGHLIGHT_OVERVIEW = ModelDecorationOptions.register({ diff --git a/src/vs/editor/contrib/multicursor/test/multicursor.test.ts b/src/vs/editor/contrib/multicursor/test/multicursor.test.ts index 6e1e0a9be..5a0e7e198 100644 --- a/src/vs/editor/contrib/multicursor/test/multicursor.test.ts +++ b/src/vs/editor/contrib/multicursor/test/multicursor.test.ts @@ -25,7 +25,7 @@ suite('Multicursor', () => { editor.setSelection(new Selection(2, 1, 2, 1)); addCursorUpAction.run(null!, editor, {}); - assert.equal(viewModel.getSelections().length, 2); + assert.strictEqual(viewModel.getSelections().length, 2); editor.trigger('test', Handler.Paste, { text: '1\n2', @@ -35,8 +35,8 @@ suite('Multicursor', () => { ] }); - assert.equal(editor.getModel()!.getLineContent(1), '1abc'); - assert.equal(editor.getModel()!.getLineContent(2), '2def'); + assert.strictEqual(editor.getModel()!.getLineContent(1), '1abc'); + assert.strictEqual(editor.getModel()!.getLineContent(2), '2def'); }); }); @@ -46,7 +46,7 @@ suite('Multicursor', () => { ], {}, (editor, viewModel) => { let addCursorDownAction = new InsertCursorBelow(); addCursorDownAction.run(null!, editor, {}); - assert.equal(viewModel.getSelections().length, 1); + assert.strictEqual(viewModel.getSelections().length, 1); }); }); @@ -90,7 +90,7 @@ suite('Multicursor selection', () => { editor.setSelection(new Selection(2, 9, 2, 16)); selectHighlightsAction.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [2, 9, 2, 16], [1, 9, 1, 16], [3, 9, 3, 16], @@ -98,7 +98,7 @@ suite('Multicursor selection', () => { editor.trigger('test', 'removeSecondaryCursors', null); - assert.deepEqual(fromRange(editor.getSelection()!), [2, 9, 2, 16]); + assert.deepStrictEqual(fromRange(editor.getSelection()!), [2, 9, 2, 16]); multiCursorSelectController.dispose(); findController.dispose(); @@ -121,13 +121,13 @@ suite('Multicursor selection', () => { findController.getState().change({ searchString: 'some+thing', isRegex: true, isRevealed: true }, false); selectHighlightsAction.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [1, 1, 1, 10], [2, 1, 2, 11], [3, 1, 3, 12], ]); - assert.equal(findController.getState().searchString, 'some+thing'); + assert.strictEqual(findController.getState().searchString, 'some+thing'); multiCursorSelectController.dispose(); findController.dispose(); @@ -154,14 +154,14 @@ suite('Multicursor selection', () => { editor.setSelection(new Selection(2, 1, 3, 4)); addSelectionToNextFindMatch.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [2, 1, 3, 4], [8, 1, 9, 4] ]); editor.trigger('test', 'removeSecondaryCursors', null); - assert.deepEqual(fromRange(editor.getSelection()!), [2, 1, 3, 4]); + assert.deepStrictEqual(fromRange(editor.getSelection()!), [2, 1, 3, 4]); multiCursorSelectController.dispose(); findController.dispose(); @@ -182,7 +182,7 @@ suite('Multicursor selection', () => { editor.setSelection(new Selection(1, 1, 1, 4)); addSelectionToNextFindMatch.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [1, 1, 1, 4], [1, 4, 1, 7] ]); @@ -190,7 +190,7 @@ suite('Multicursor selection', () => { addSelectionToNextFindMatch.run(null!, editor); addSelectionToNextFindMatch.run(null!, editor); addSelectionToNextFindMatch.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [1, 1, 1, 4], [1, 4, 1, 7], [2, 1, 2, 4], @@ -199,14 +199,14 @@ suite('Multicursor selection', () => { ]); editor.trigger('test', Handler.Type, { text: 'z' }); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [1, 2, 1, 2], [1, 3, 1, 3], [2, 2, 2, 2], [3, 2, 3, 2], [3, 3, 3, 3] ]); - assert.equal(editor.getValue(), [ + assert.strictEqual(editor.getValue(), [ 'zz', 'z', 'zz', @@ -239,14 +239,14 @@ suite('Multicursor selection', () => { editor.setSelection(new Selection(2, 1, 3, 4)); addSelectionToNextFindMatch.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [2, 1, 3, 4], [8, 1, 9, 4] ]); editor.trigger('test', 'removeSecondaryCursors', null); - assert.deepEqual(fromRange(editor.getSelection()!), [2, 1, 3, 4]); + assert.deepStrictEqual(fromRange(editor.getSelection()!), [2, 1, 3, 4]); multiCursorSelectController.dispose(); findController.dispose(); @@ -284,25 +284,25 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), @@ -323,20 +323,20 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), @@ -357,20 +357,20 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), @@ -392,14 +392,14 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), @@ -421,14 +421,14 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 5, 1, 10), new Selection(2, 5, 2, 10), new Selection(3, 5, 3, 8), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 5, 1, 10), new Selection(2, 5, 2, 10), new Selection(3, 5, 3, 8), @@ -450,20 +450,20 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), new Selection(3, 1, 3, 5), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), new Selection(3, 1, 3, 5), @@ -471,7 +471,7 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), new Selection(3, 1, 3, 5), @@ -480,7 +480,7 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), new Selection(3, 1, 3, 5), @@ -508,18 +508,18 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), new Selection(6, 2, 6, 5), @@ -534,12 +534,12 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), ]); @@ -550,7 +550,7 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), ]); @@ -565,14 +565,14 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), new Selection(6, 2, 6, 5), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), new Selection(6, 2, 6, 5), diff --git a/src/vs/editor/contrib/parameterHints/parameterHints.css b/src/vs/editor/contrib/parameterHints/parameterHints.css index 03c4e2640..64f277ccf 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/parameterHints.css @@ -10,7 +10,7 @@ line-height: 1.5em; } -.monaco-editor .parameter-hints-widget > .wrapper { +.monaco-editor .parameter-hints-widget > .phwrapper { max-width: 440px; display: flex; flex-direction: row; diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index 54b7a807b..2f41f4892 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -14,7 +14,7 @@ import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentW import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import * as modes from 'vs/editor/common/modes'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { Context } from 'vs/editor/contrib/parameterHints/provideSignatureHelp'; import * as nls from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -27,6 +27,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { assertIsDefined } from 'vs/base/common/types'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; const $ = dom.$; @@ -81,7 +82,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { private createParamaterHintDOMNodes() { const element = $('.editor-widget.parameter-hints-widget'); - const wrapper = dom.append(element, $('.wrapper')); + const wrapper = dom.append(element, $('.phwrapper')); wrapper.tabIndex = -1; const controls = dom.append(wrapper, $('.controls')); @@ -225,8 +226,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { if (typeof activeParameter.documentation === 'string') { documentation.textContent = activeParameter.documentation; } else { - const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(activeParameter.documentation)); - renderedContents.element.classList.add('markdown-docs'); + const renderedContents = this.renderMarkdownDocs(activeParameter.documentation); documentation.appendChild(renderedContents.element); } dom.append(this.domNodes.docs, $('p', {}, documentation)); @@ -237,8 +237,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } else if (typeof signature.documentation === 'string') { dom.append(this.domNodes.docs, $('p', {}, signature.documentation)); } else { - const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(signature.documentation)); - renderedContents.element.classList.add('markdown-docs'); + const renderedContents = this.renderMarkdownDocs(signature.documentation); dom.append(this.domNodes.docs, renderedContents.element); } @@ -265,6 +264,16 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { this.domNodes.scrollbar.scanDomNode(); } + private renderMarkdownDocs(markdown: IMarkdownString | undefined): IMarkdownRenderResult { + const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(markdown, { + asyncRenderCallback: () => { + this.domNodes?.scrollbar.scanDomNode(); + } + })); + renderedContents.element.classList.add('markdown-docs'); + return renderedContents; + } + private hasDocs(signature: modes.SignatureInformation, activeParameter: modes.ParameterInformation | undefined): boolean { if (activeParameter && typeof activeParameter.documentation === 'string' && assertIsDefined(activeParameter.documentation).length > 0) { return true; @@ -360,7 +369,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { const height = Math.max(this.editor.getLayoutInfo().height / 4, 250); const maxHeight = `${height}px`; this.domNodes.element.style.maxHeight = maxHeight; - const wrapper = this.domNodes.element.getElementsByClassName('wrapper') as HTMLCollectionOf; + const wrapper = this.domNodes.element.getElementsByClassName('phwrapper') as HTMLCollectionOf; if (wrapper.length) { wrapper[0].style.maxHeight = maxHeight; } diff --git a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts index d513ef4b9..6cfa1e420 100644 --- a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts +++ b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { first } from 'vs/base/common/async'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; @@ -20,19 +19,26 @@ export const Context = { MultipleSignatures: new RawContextKey('parameterHintsMultipleSignatures', false), }; -export function provideSignatureHelp( +export async function provideSignatureHelp( model: ITextModel, position: Position, context: modes.SignatureHelpContext, token: CancellationToken -): Promise { +): Promise { const supports = modes.SignatureHelpProviderRegistry.ordered(model); - return first(supports.map(support => () => { - return Promise.resolve(support.provideSignatureHelp(model, position, token, context)) - .catch(e => onUnexpectedExternalError(e)); - })); + for (const support of supports) { + try { + const result = await support.provideSignatureHelp(model, position, token, context); + if (result) { + return result; + } + } catch (err) { + onUnexpectedExternalError(err); + } + } + return undefined; } CommandsRegistry.registerCommand('_executeSignatureHelpProvider', async (accessor, ...args: [URI, IPosition, string?]) => { diff --git a/src/vs/editor/contrib/peekView/peekView.ts b/src/vs/editor/contrib/peekView/peekView.ts index f265a4bfd..36db5b2b3 100644 --- a/src/vs/editor/contrib/peekView/peekView.ts +++ b/src/vs/editor/contrib/peekView/peekView.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/peekViewWidget'; import * as dom from 'vs/base/browser/dom'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { ActionBar, IActionBarOptions } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar, ActionsOrientation, IActionBarOptions } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action } from 'vs/base/common/actions'; import { Color } from 'vs/base/common/color'; import { Emitter } from 'vs/base/common/event'; @@ -25,8 +25,7 @@ import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { registerColor, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { Codicon } from 'vs/base/common/codicons'; -import { MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; -import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; export const IPeekViewService = createDecorator('IPeekViewService'); export interface IPeekViewService { @@ -107,6 +106,7 @@ export abstract class PeekViewWidget extends ZoneWidget { private readonly _onDidClose = new Emitter(); readonly onDidClose = this._onDidClose.event; + private disposed?: true; protected _headElement?: HTMLDivElement; protected _primaryHeading?: HTMLElement; @@ -125,8 +125,11 @@ export abstract class PeekViewWidget extends ZoneWidget { } dispose(): void { - super.dispose(); - this._onDidClose.fire(this); + if (!this.disposed) { + this.disposed = true; // prevent consumers who dispose on onDidClose from looping + super.dispose(); + this._onDidClose.fire(this); + } } style(styles: IPeekViewStyles): void { @@ -204,15 +207,8 @@ export abstract class PeekViewWidget extends ZoneWidget { protected _getActionBarOptions(): IActionBarOptions { return { - actionViewItemProvider: action => { - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - - return undefined; - } + actionViewItemProvider: createActionViewItem.bind(undefined, this.instantiationService), + orientation: ActionsOrientation.HORIZONTAL }; } diff --git a/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts b/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts index 7db794fa8..ec7d2ab92 100644 --- a/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts @@ -10,7 +10,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { stripCodicons } from 'vs/base/common/codicons'; +import { stripIcons } from 'vs/base/common/iconLabels'; export abstract class AbstractEditorCommandsQuickAccessProvider extends AbstractCommandsQuickAccessProvider { @@ -41,7 +41,7 @@ export abstract class AbstractEditorCommandsQuickAccessProvider extends Abstract editorCommandPicks.push({ commandId: editorAction.id, commandAlias: editorAction.alias, - label: stripCodicons(editorAction.label) || editorAction.id, + label: stripIcons(editorAction.label) || editorAction.id, }); } diff --git a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts index 9fb3e3d3c..96c0a9be6 100644 --- a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts @@ -12,11 +12,10 @@ import { ITextModel } from 'vs/editor/common/model'; import { IRange, Range } from 'vs/editor/common/core/range'; import { AbstractEditorNavigationQuickAccessProvider, IEditorNavigationQuickAccessOptions, IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; import { DocumentSymbol, SymbolKinds, SymbolTag, DocumentSymbolProviderRegistry, SymbolKind } from 'vs/editor/common/modes'; -import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { trim, format } from 'vs/base/common/strings'; import { prepareQuery, IPreparedQuery, pieceToQuery, scoreFuzzy2 } from 'vs/base/common/fuzzyScorer'; import { IMatch } from 'vs/base/common/filters'; -import { Iterable } from 'vs/base/common/iterator'; import { Codicon } from 'vs/base/common/codicons'; export interface IGotoSymbolQuickPickItem extends IQuickPickItem { @@ -144,7 +143,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit // Resolve symbols from document once and reuse this // request for all filtering and typing then on - const symbolsPromise = this.getDocumentSymbols(model, true, token); + const symbolsPromise = this.getDocumentSymbols(model, token); // Set initial picks and update on type let picksCts: CancellationTokenSource | undefined = undefined; @@ -418,49 +417,9 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return result; } - protected async getDocumentSymbols(document: ITextModel, flatten: boolean, token: CancellationToken): Promise { + protected async getDocumentSymbols(document: ITextModel, token: CancellationToken): Promise { const model = await OutlineModel.create(document, token); - if (token.isCancellationRequested) { - return []; - } - - const roots: DocumentSymbol[] = []; - for (const child of model.children.values()) { - if (child instanceof OutlineElement) { - roots.push(child.symbol); - } else { - roots.push(...Iterable.map(child.children.values(), child => child.symbol)); - } - } - - let flatEntries: DocumentSymbol[] = []; - if (flatten) { - this.flattenDocumentSymbols(flatEntries, roots, ''); - } else { - flatEntries = roots; - } - - return flatEntries.sort((symbolA, symbolB) => Range.compareRangesUsingStarts(symbolA.range, symbolB.range)); - } - - private flattenDocumentSymbols(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void { - for (const entry of entries) { - bucket.push({ - kind: entry.kind, - tags: entry.tags, - name: entry.name, - detail: entry.detail, - containerName: entry.containerName || overrideContainerLabel, - range: entry.range, - selectionRange: entry.selectionRange, - children: undefined, // we flatten it... - }); - - // Recurse over children - if (entry.children) { - this.flattenDocumentSymbols(bucket, entry.children, entry.name); - } - } + return token.isCancellationRequested ? [] : model.asListOfDocumentSymbols(); } } diff --git a/src/vs/editor/contrib/smartSelect/bracketSelections.ts b/src/vs/editor/contrib/smartSelect/bracketSelections.ts index 3842ad87b..af7496f4c 100644 --- a/src/vs/editor/contrib/smartSelect/bracketSelections.ts +++ b/src/vs/editor/contrib/smartSelect/bracketSelections.ts @@ -26,7 +26,7 @@ export class BracketSelectionRangeProvider implements SelectionRangeProvider { return result; } - private static readonly _maxDuration = 30; + public static _maxDuration = 30; private static readonly _maxRounds = 2; private static _bracketsRightYield(resolve: () => void, round: number, model: ITextModel, pos: Position, ranges: Map>): void { diff --git a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts index 4c566492b..862a0febe 100644 --- a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts @@ -16,7 +16,7 @@ import { BracketSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/bra import { provideSelectionRanges } from 'vs/editor/contrib/smartSelect/smartSelect'; import { CancellationToken } from 'vs/base/common/cancellation'; import { WordSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/wordSelections'; -import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/modelService.test'; +import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { NullLogService } from 'vs/platform/log/common/log'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; @@ -45,6 +45,16 @@ class MockJSMode extends MockMode { suite('SmartSelect', () => { + const OriginalBracketSelectionRangeProviderMaxDuration = BracketSelectionRangeProvider._maxDuration; + + suiteSetup(() => { + BracketSelectionRangeProvider._maxDuration = 5000; // 5 seconds + }); + + suiteTeardown(() => { + BracketSelectionRangeProvider._maxDuration = OriginalBracketSelectionRangeProviderMaxDuration; + }); + let modelService: ModelServiceImpl; let mode: MockJSMode; diff --git a/src/vs/editor/contrib/snippet/snippetVariables.ts b/src/vs/editor/contrib/snippet/snippetVariables.ts index bdfe96e7c..0113f8636 100644 --- a/src/vs/editor/contrib/snippet/snippetVariables.ts +++ b/src/vs/editor/contrib/snippet/snippetVariables.ts @@ -12,11 +12,11 @@ import { VariableResolver, Variable, Text } from 'vs/editor/contrib/snippet/snip import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { getLeadingWhitespace, commonPrefixLength, isFalsyOrWhitespace, splitLines } from 'vs/base/common/strings'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier, WORKSPACE_EXTENSION, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { toWorkspaceIdentifier, WORKSPACE_EXTENSION, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ILabelService } from 'vs/platform/label/common/label'; import { normalizeDriveLetter } from 'vs/base/common/labels'; -import { URI } from 'vs/base/common/uri'; import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; +import { generateUuid } from 'vs/base/common/uuid'; export const KnownSnippetVariableNames: { [key: string]: true } = Object.freeze({ 'CURRENT_YEAR': true, @@ -42,6 +42,7 @@ export const KnownSnippetVariableNames: { [key: string]: true } = Object.freeze( 'TM_FILENAME_BASE': true, 'TM_DIRECTORY': true, 'TM_FILEPATH': true, + 'RELATIVE_FILEPATH': true, 'BLOCK_COMMENT_START': true, 'BLOCK_COMMENT_END': true, 'LINE_COMMENT': true, @@ -49,6 +50,7 @@ export const KnownSnippetVariableNames: { [key: string]: true } = Object.freeze( 'WORKSPACE_FOLDER': true, 'RANDOM': true, 'RANDOM_HEX': true, + 'UUID': true }); export class CompositeSnippetVariableResolver implements VariableResolver { @@ -177,6 +179,8 @@ export class ModelBasedVariableResolver implements VariableResolver { } else if (name === 'TM_FILEPATH' && this._labelService) { return this._labelService.getUriLabel(this._model.uri); + } else if (name === 'RELATIVE_FILEPATH' && this._labelService) { + return this._labelService.getUriLabel(this._model.uri, { relative: true, noPrefix: true }); } return undefined; @@ -309,9 +313,9 @@ export class WorkspaceBasedVariableResolver implements VariableResolver { return undefined; } - private _resolveWorkspaceName(workspaceIdentifier: IWorkspaceIdentifier | URI): string | undefined { + private _resolveWorkspaceName(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string | undefined { if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { - return path.basename(workspaceIdentifier.path); + return path.basename(workspaceIdentifier.uri.path); } let filename = path.basename(workspaceIdentifier.configPath.path); @@ -320,9 +324,9 @@ export class WorkspaceBasedVariableResolver implements VariableResolver { } return filename; } - private _resoveWorkspacePath(workspaceIdentifier: IWorkspaceIdentifier | URI): string | undefined { + private _resoveWorkspacePath(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string | undefined { if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { - return normalizeDriveLetter(workspaceIdentifier.fsPath); + return normalizeDriveLetter(workspaceIdentifier.uri.fsPath); } let filename = path.basename(workspaceIdentifier.configPath.path); @@ -340,9 +344,10 @@ export class RandomBasedVariableResolver implements VariableResolver { if (name === 'RANDOM') { return Math.random().toString().slice(-6); - } - else if (name === 'RANDOM_HEX') { + } else if (name === 'RANDOM_HEX') { return Math.random().toString(16).slice(-6); + } else if (name === 'UUID') { + return generateUuid(); } return undefined; diff --git a/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts b/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts index 8c9be33cf..2559a4219 100644 --- a/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts @@ -62,34 +62,34 @@ suite('SnippetController', () => { editor.setPosition({ lineNumber: 4, column: 2 }); snippetController.insert(template); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var index; index < array.length; index++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = array[index];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var index; index < array.length; index++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = array[index];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); editor.trigger('test', 'type', { text: 'i' }); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var i; i < array.length; i++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = array[i];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var i; i < array.length; i++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = array[i];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); snippetController.next(); editor.trigger('test', 'type', { text: 'arr' }); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var i; i < arr.length; i++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = arr[i];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var i; i < arr.length; i++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = arr[i];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); snippetController.prev(); editor.trigger('test', 'type', { text: 'j' }); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var j; j < arr.length; j++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = arr[j];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var j; j < arr.length; j++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = arr[j];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); snippetController.next(); snippetController.next(); - assert.deepEqual(editor.getPosition(), new Position(6, 3)); + assert.deepStrictEqual(editor.getPosition(), new Position(6, 3)); }); }); @@ -98,13 +98,13 @@ suite('SnippetController', () => { editor.setPosition({ lineNumber: 4, column: 2 }); snippetController.insert(template); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var index; index < array.length; index++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = array[index];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var index; index < array.length; index++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = array[index];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); snippetController.cancel(); - assert.deepEqual(editor.getPosition(), new Position(4, 16)); + assert.deepStrictEqual(editor.getPosition(), new Position(4, 16)); }); }); @@ -121,7 +121,7 @@ suite('SnippetController', () => { // text: null // }]); - // assert.equal(snippetController.isInSnippetMode(), false); + // assert.strictEqual(snippetController.isInSnippetMode(), false); // }); // }); @@ -138,7 +138,7 @@ suite('SnippetController', () => { // text: null // }]); - // assert.equal(snippetController.isInSnippetMode(), false); + // assert.strictEqual(snippetController.isInSnippetMode(), false); // }); // }); @@ -155,7 +155,7 @@ suite('SnippetController', () => { // text: '\nHello' // }]); - // assert.equal(snippetController.isInSnippetMode(), false); + // assert.strictEqual(snippetController.isInSnippetMode(), false); // }); // }); @@ -172,7 +172,7 @@ suite('SnippetController', () => { // text: '\nHello' // }]); - // assert.equal(snippetController.isInSnippetMode(), false); + // assert.strictEqual(snippetController.isInSnippetMode(), false); // }); // }); @@ -183,7 +183,7 @@ suite('SnippetController', () => { editor.getModel()!.setValue('goodbye'); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -194,7 +194,7 @@ suite('SnippetController', () => { editor.getModel()!.undo(); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -205,7 +205,7 @@ suite('SnippetController', () => { editor.setPosition({ lineNumber: 1, column: 1 }); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -216,7 +216,7 @@ suite('SnippetController', () => { editor.setModel(null); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -227,7 +227,7 @@ suite('SnippetController', () => { snippetController.dispose(); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -241,7 +241,7 @@ suite('SnippetController', () => { codeSnippet = 'foo$0'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 2, startColumn: 4, endLineNumber: 2, endColumn: 4 }), second.toString()); @@ -256,7 +256,7 @@ suite('SnippetController', () => { codeSnippet = 'foo$0bar'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 2, startColumn: 4, endLineNumber: 2, endColumn: 4 }), second.toString()); @@ -271,7 +271,7 @@ suite('SnippetController', () => { codeSnippet = 'foo$0bar'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 1, startColumn: 14, endLineNumber: 1, endColumn: 14 }), second.toString()); @@ -286,7 +286,7 @@ suite('SnippetController', () => { codeSnippet = 'foo\n$0\nbar'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 4, startColumn: 1, endLineNumber: 4, endColumn: 1 }), second.toString()); @@ -301,7 +301,7 @@ suite('SnippetController', () => { codeSnippet = 'foo\n$0\nbar'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 4, startColumn: 1, endLineNumber: 4, endColumn: 1 }), second.toString()); @@ -315,7 +315,7 @@ suite('SnippetController', () => { codeSnippet = 'xo$0r'; snippetController.insert(codeSnippet, { overwriteBefore: 1 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 2, startColumn: 8, endColumn: 8, endLineNumber: 2 })); }); }); @@ -328,9 +328,9 @@ suite('SnippetController', () => { codeSnippet = '{{% url_**$1** %}}'; controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 1, startColumn: 27, endLineNumber: 1, endColumn: 27 })); - assert.equal(editor.getModel()!.getValue(), 'example example {{% url_**** %}}'); + assert.strictEqual(editor.getModel()!.getValue(), 'example example {{% url_**** %}}'); }, ['example example sc']); @@ -346,9 +346,9 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 2, startColumn: 2, endLineNumber: 2, endColumn: 2 }), editor.getSelection()!.toString()); - assert.equal(editor.getModel()!.getValue(), 'afterEach((done) => {\n\ttest\n});'); + assert.strictEqual(editor.getModel()!.getValue(), 'afterEach((done) => {\n\ttest\n});'); }, ['af']); @@ -364,9 +364,9 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }), editor.getSelection()!.toString()); - assert.equal(editor.getModel()!.getValue(), 'afterEach((done) => {\n\ttest\n});'); + assert.strictEqual(editor.getModel()!.getValue(), 'afterEach((done) => {\n\ttest\n});'); }, ['af']); @@ -380,8 +380,8 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 8 }); - assert.equal(editor.getModel()!.getValue(), 'after'); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getModel()!.getValue(), 'after'); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), editor.getSelection()!.toString()); }, ['afterone']); @@ -404,7 +404,7 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 5, startColumn: 3, endLineNumber: 5, endColumn: 3 }), first.toString()); @@ -429,7 +429,7 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); const [first] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 2, startColumn: 3, endLineNumber: 2, endColumn: 3 }), first.toString()); @@ -465,7 +465,7 @@ suite('SnippetController', () => { codeSnippet = '_foo'; controller.insert(codeSnippet, { overwriteBefore: 1 }); - assert.equal(editor.getModel()!.getValue(), 'this._foo\nabc_foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._foo\nabc_foo'); }, ['this._', 'abc']); @@ -478,7 +478,7 @@ suite('SnippetController', () => { codeSnippet = 'XX'; controller.insert(codeSnippet, { overwriteBefore: 1 }); - assert.equal(editor.getModel()!.getValue(), 'this.XX\nabcXX'); + assert.strictEqual(editor.getModel()!.getValue(), 'this.XX\nabcXX'); }, ['this._', 'abc']); @@ -492,7 +492,7 @@ suite('SnippetController', () => { codeSnippet = '_foo'; controller.insert(codeSnippet, { overwriteBefore: 1 }); - assert.equal(editor.getModel()!.getValue(), 'this._foo\nabc_foo\ndef_foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._foo\nabc_foo\ndef_foo'); }, ['this._', 'abc', 'def_']); @@ -506,7 +506,7 @@ suite('SnippetController', () => { codeSnippet = '._foo'; controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getModel()!.getValue(), 'this._foo\nabc._foo\ndef._foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._foo\nabc._foo\ndef._foo'); }, ['this._', 'abc', 'def._']); @@ -520,7 +520,7 @@ suite('SnippetController', () => { codeSnippet = '._foo'; controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getModel()!.getValue(), 'this._foo\nabc._foo\ndef._foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._foo\nabc._foo\ndef._foo'); }, ['this._', 'abc', 'def._']); @@ -534,7 +534,7 @@ suite('SnippetController', () => { codeSnippet = '._foo'; controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getModel()!.getValue(), 'this._._foo\na._foo\ndef._._foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._._foo\na._foo\ndef._._foo'); }, ['this._', 'abc', 'def._']); @@ -550,7 +550,7 @@ suite('SnippetController', () => { codeSnippet = 'document'; controller.insert(codeSnippet, { overwriteBefore: 3 }); - assert.equal(editor.getModel()!.getValue(), '{document}\n{document && true}'); + assert.strictEqual(editor.getModel()!.getValue(), '{document}\n{document && true}'); }, ['{foo}', '{foo && true}']); }); @@ -565,7 +565,7 @@ suite('SnippetController', () => { codeSnippet = 'for (var ${1:i}=0; ${1:i} { codeSnippet = 'for (let ${1:i}=0; ${1:i} expected=${actual.toString()}`); } - assert.equal(s.length, 0); + assert.strictEqual(s.length, 0); } function assertContextKeys(service: MockContextKeyService, inSnippet: boolean, hasPrev: boolean, hasNext: boolean): void { - assert.equal(SnippetController2.InSnippetMode.getValue(service), inSnippet, `inSnippetMode`); - assert.equal(SnippetController2.HasPrevTabstop.getValue(service), hasPrev, `HasPrevTabstop`); - assert.equal(SnippetController2.HasNextTabstop.getValue(service), hasNext, `HasNextTabstop`); + assert.strictEqual(SnippetController2.InSnippetMode.getValue(service), inSnippet, `inSnippetMode`); + assert.strictEqual(SnippetController2.HasPrevTabstop.getValue(service), hasPrev, `HasPrevTabstop`); + assert.strictEqual(SnippetController2.HasNextTabstop.getValue(service), hasNext, `HasNextTabstop`); } let editor: ICodeEditor; @@ -40,7 +40,7 @@ suite('SnippetController2', function () { model = createTextModel('if\n $state\nfi'); editor = createTestCodeEditor({ model: model }); editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)]); - assert.equal(model.getEOL(), '\n'); + assert.strictEqual(model.getEOL(), '\n'); }); teardown(function () { @@ -78,9 +78,9 @@ suite('SnippetController2', function () { assertContextKeys(contextKeys, false, false, false); editor.trigger('test', 'type', { text: '\t' }); - assert.equal(SnippetController2.InSnippetMode.getValue(contextKeys), false); - assert.equal(SnippetController2.HasNextTabstop.getValue(contextKeys), false); - assert.equal(SnippetController2.HasPrevTabstop.getValue(contextKeys), false); + assert.strictEqual(SnippetController2.InSnippetMode.getValue(contextKeys), false); + assert.strictEqual(SnippetController2.HasNextTabstop.getValue(contextKeys), false); + assert.strictEqual(SnippetController2.HasPrevTabstop.getValue(contextKeys), false); }); test('insert, insert -> cursor moves out (left/right)', function () { @@ -111,7 +111,7 @@ suite('SnippetController2', function () { const ctrl = new SnippetController2(editor, logService, contextKeys); ctrl.insert('foo${1:bar}foo$0'); - assert.equal(SnippetController2.InSnippetMode.getValue(contextKeys), true); + assert.strictEqual(SnippetController2.InSnippetMode.getValue(contextKeys), true); assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11)); // bad selection change diff --git a/src/vs/editor/contrib/snippet/test/snippetParser.test.ts b/src/vs/editor/contrib/snippet/test/snippetParser.test.ts index ba019cdc3..ed5dcc443 100644 --- a/src/vs/editor/contrib/snippet/test/snippetParser.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetParser.test.ts @@ -531,8 +531,8 @@ suite('SnippetParser', () => { let snippet = new SnippetParser().parse('This ${1:is ${2:nested}}$0', true); let [first, second] = snippet.placeholders; - assert.deepEqual(snippet.enclosingPlaceholders(first), []); - assert.deepEqual(snippet.enclosingPlaceholders(second), [first]); + assert.deepStrictEqual(snippet.enclosingPlaceholders(first), []); + assert.deepStrictEqual(snippet.enclosingPlaceholders(second), [first]); }); test('TextmateSnippet#offset', () => { diff --git a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts index 458555cb0..a65de3d53 100644 --- a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts @@ -23,14 +23,14 @@ suite('SnippetSession', function () { const actual = s.shift()!; assert.ok(selection.equalsSelection(actual), `actual=${selection.toString()} <> expected=${actual.toString()}`); } - assert.equal(s.length, 0); + assert.strictEqual(s.length, 0); } setup(function () { model = createTextModel('function foo() {\n console.log(a);\n}'); editor = createTestCodeEditor({ model: model }) as IActiveCodeEditor; editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)]); - assert.equal(model.getEOL(), '\n'); + assert.strictEqual(model.getEOL(), '\n'); }); teardown(function () { @@ -43,7 +43,7 @@ suite('SnippetSession', function () { function assertNormalized(position: IPosition, input: string, expected: string): void { const snippet = new SnippetParser().parse(input); SnippetSession.adjustWhitespace(model, position, snippet, true, true); - assert.equal(snippet.toTextmateString(), expected); + assert.strictEqual(snippet.toTextmateString(), expected); } assertNormalized(new Position(1, 1), 'foo', 'foo'); @@ -73,7 +73,7 @@ suite('SnippetSession', function () { test('text edits & selection', function () { const session = new SnippetSession(editor, 'foo${1:bar}foo$0'); session.insert(); - assert.equal(editor.getModel()!.getValue(), 'foobarfoofunction foo() {\n foobarfooconsole.log(a);\n}'); + assert.strictEqual(editor.getModel()!.getValue(), 'foobarfoofunction foo() {\n foobarfooconsole.log(a);\n}'); assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11)); session.next(); @@ -86,7 +86,7 @@ suite('SnippetSession', function () { editor.setSelections([new Selection(2, 5, 2, 5), new Selection(1, 1, 1, 1)]); session.insert(); - assert.equal(model.getValue(), 'barfunction foo() {\n barconsole.log(a);\n}'); + assert.strictEqual(model.getValue(), 'barfunction foo() {\n barconsole.log(a);\n}'); assertSelections(editor, new Selection(2, 5, 2, 8), new Selection(1, 1, 1, 4)); }); @@ -107,7 +107,7 @@ suite('SnippetSession', function () { test('snippets, just text', function () { const session = new SnippetSession(editor, 'foobar'); session.insert(); - assert.equal(model.getValue(), 'foobarfunction foo() {\n foobarconsole.log(a);\n}'); + assert.strictEqual(model.getValue(), 'foobarfunction foo() {\n foobarconsole.log(a);\n}'); assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); }); @@ -116,7 +116,7 @@ suite('SnippetSession', function () { const session = new SnippetSession(editor, 'foo\n\t${1:bar}\n$0'); session.insert(); - assert.equal(editor.getModel()!.getValue(), 'foo\n bar\nfunction foo() {\n foo\n bar\n console.log(a);\n}'); + assert.strictEqual(editor.getModel()!.getValue(), 'foo\n bar\nfunction foo() {\n foo\n bar\n console.log(a);\n}'); assertSelections(editor, new Selection(2, 5, 2, 8), new Selection(5, 9, 5, 12)); @@ -129,7 +129,7 @@ suite('SnippetSession', function () { editor.setSelection(new Selection(2, 5, 2, 5)); const session = new SnippetSession(editor, 'abc\n foo\n bar\n$0', { overwriteBefore: 0, overwriteAfter: 0, adjustWhitespace: false, clipboardText: undefined, overtypingCapturer: undefined }); session.insert(); - assert.equal(editor.getModel()!.getValue(), 'function foo() {\n abc\n foo\n bar\nconsole.log(a);\n}'); + assert.strictEqual(editor.getModel()!.getValue(), 'function foo() {\n abc\n foo\n bar\nconsole.log(a);\n}'); }); test('snippets, selections -> next/prev', () => { @@ -171,7 +171,7 @@ suite('SnippetSession', function () { // go to final tabstop session.next(); - assert.equal(model.getValue(), 'fX_bar_function foo() {\n fX_bar_console.log(a);\n}'); + assert.strictEqual(model.getValue(), 'fX_bar_function foo() {\n fX_bar_console.log(a);\n}'); assertSelections(editor, new Selection(1, 8, 1, 8), new Selection(2, 12, 2, 12)); }); @@ -180,7 +180,7 @@ suite('SnippetSession', function () { editor.setSelections([new Selection(1, 1, 1, 4), new Selection(1, 9, 1, 12)]); new SnippetSession(editor, 'x$0').insert(); - assert.equal(model.getValue(), 'x_bar_x'); + assert.strictEqual(model.getValue(), 'x_bar_x'); assertSelections(editor, new Selection(1, 2, 1, 2), new Selection(1, 8, 1, 8)); }); @@ -189,7 +189,7 @@ suite('SnippetSession', function () { editor.setSelections([new Selection(1, 1, 1, 4), new Selection(1, 9, 1, 12)]); new SnippetSession(editor, 'LONGER$0').insert(); - assert.equal(model.getValue(), 'LONGER_bar_LONGER'); + assert.strictEqual(model.getValue(), 'LONGER_bar_LONGER'); assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(1, 18, 1, 18)); }); @@ -203,11 +203,11 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: 'foo-' }); session.next(); - assert.equal(model.getValue(), 'foo_foo-bar_foo'); + assert.strictEqual(model.getValue(), 'foo_foo-bar_foo'); assertSelections(editor, new Selection(1, 12, 1, 12)); editor.trigger('test', 'type', { text: 'XXX' }); - assert.equal(model.getValue(), 'foo_foo-barXXX_foo'); + assert.strictEqual(model.getValue(), 'foo_foo-barXXX_foo'); session.prev(); assertSelections(editor, new Selection(1, 5, 1, 9)); session.next(); @@ -242,7 +242,7 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: '333' }); session.next(); - assert.equal(model.getValue(), '111222333function foo() {\n 111222333console.log(a);\n}'); + assert.strictEqual(model.getValue(), '111222333function foo() {\n 111222333console.log(a);\n}'); assertSelections(editor, new Selection(1, 10, 1, 10), new Selection(2, 14, 2, 14)); session.prev(); @@ -269,22 +269,22 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: '333' }); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); }); test('snippets, gracefully move over final tabstop', function () { const session = new SnippetSession(editor, '${1}bar$0'); session.insert(); - assert.equal(session.isAtLastPlaceholder, false); + assert.strictEqual(session.isAtLastPlaceholder, false); assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); }); @@ -294,46 +294,46 @@ suite('SnippetSession', function () { assertSelections(editor, new Selection(1, 5, 1, 7), new Selection(2, 9, 2, 11)); editor.trigger('test', 'type', { text: 'XXX' }); - assert.equal(model.getValue(), 'log(XXX);function foo() {\n log(XXX);console.log(a);\n}'); + assert.strictEqual(model.getValue(), 'log(XXX);function foo() {\n log(XXX);console.log(a);\n}'); session.next(); - assert.equal(session.isAtLastPlaceholder, false); + assert.strictEqual(session.isAtLastPlaceholder, false); // assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 10, 1, 10), new Selection(2, 14, 2, 14)); }); test('snippets, selections and snippet ranges', function () { const session = new SnippetSession(editor, '${1:foo}farboo${2:bar}$0'); session.insert(); - assert.equal(model.getValue(), 'foofarboobarfunction foo() {\n foofarboobarconsole.log(a);\n}'); + assert.strictEqual(model.getValue(), 'foofarboobarfunction foo() {\n foofarboobarconsole.log(a);\n}'); assertSelections(editor, new Selection(1, 1, 1, 4), new Selection(2, 5, 2, 8)); - assert.equal(session.isSelectionWithinPlaceholders(), true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), true); editor.setSelections([new Selection(1, 1, 1, 1)]); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); editor.setSelections([new Selection(1, 6, 1, 6), new Selection(2, 10, 2, 10)]); - assert.equal(session.isSelectionWithinPlaceholders(), false); // in snippet, outside placeholder + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); // in snippet, outside placeholder editor.setSelections([new Selection(1, 6, 1, 6), new Selection(2, 10, 2, 10), new Selection(1, 1, 1, 1)]); - assert.equal(session.isSelectionWithinPlaceholders(), false); // in snippet, outside placeholder + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); // in snippet, outside placeholder editor.setSelections([new Selection(1, 6, 1, 6), new Selection(2, 10, 2, 10), new Selection(2, 20, 2, 21)]); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); // reset selection to placeholder session.next(); - assert.equal(session.isSelectionWithinPlaceholders(), true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), true); assertSelections(editor, new Selection(1, 10, 1, 13), new Selection(2, 14, 2, 17)); // reset selection to placeholder session.next(); - assert.equal(session.isSelectionWithinPlaceholders(), true); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 13, 1, 13), new Selection(2, 17, 2, 17)); }); @@ -344,20 +344,20 @@ suite('SnippetSession', function () { const first = new SnippetSession(editor, 'foo${2:bar}foo$0'); first.insert(); - assert.equal(model.getValue(), 'foobarfoo'); + assert.strictEqual(model.getValue(), 'foobarfoo'); assertSelections(editor, new Selection(1, 4, 1, 7)); const second = new SnippetSession(editor, 'ba${1:zzzz}$0'); second.insert(); - assert.equal(model.getValue(), 'foobazzzzfoo'); + assert.strictEqual(model.getValue(), 'foobazzzzfoo'); assertSelections(editor, new Selection(1, 6, 1, 10)); second.next(); - assert.equal(second.isAtLastPlaceholder, true); + assert.strictEqual(second.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 10, 1, 10)); first.next(); - assert.equal(first.isAtLastPlaceholder, true); + assert.strictEqual(first.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 13, 1, 13)); }); @@ -365,11 +365,11 @@ suite('SnippetSession', function () { const session = new SnippetSession(editor, 'farboo$0'); session.insert(); - assert.equal(session.isAtLastPlaceholder, true); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); editor.trigger('test', 'type', { text: 'XXX' }); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); }); test('snippets, typing at beginning', function () { @@ -379,12 +379,12 @@ suite('SnippetSession', function () { session.insert(); editor.setSelection(new Selection(1, 2, 1, 2)); - assert.equal(session.isSelectionWithinPlaceholders(), false); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isAtLastPlaceholder, true); editor.trigger('test', 'type', { text: 'XXX' }); - assert.equal(model.getLineContent(1), 'fXXXfarboounction foo() {'); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(model.getLineContent(1), 'fXXXfarboounction foo() {'); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); session.next(); assertSelections(editor, new Selection(1, 11, 1, 11)); @@ -412,7 +412,7 @@ suite('SnippetSession', function () { const session = new SnippetSession(editor, '@line=$TM_LINE_NUMBER$0'); session.insert(); - assert.equal(model.getValue(), '@line=1function foo() {\n @line=2console.log(a);\n}'); + assert.strictEqual(model.getValue(), '@line=1function foo() {\n @line=2console.log(a);\n}'); assertSelections(editor, new Selection(1, 8, 1, 8), new Selection(2, 12, 2, 12)); }); @@ -428,10 +428,10 @@ suite('SnippetSession', function () { session.next(); assertSelections(editor, new Selection(1, 22, 1, 22)); - assert.equal(session.isAtLastPlaceholder, false); + assert.strictEqual(session.isAtLastPlaceholder, false); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 23, 1, 23)); session.prev(); @@ -456,8 +456,8 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: 'foo' }); session.next(); - assert.equal(model.getValue(), 'bar'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), 'bar'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 4, 1, 4)); }); @@ -471,8 +471,8 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: 'foo' }); session.next(); - assert.equal(model.getValue(), 'foo baz bar'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), 'foo baz bar'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 12, 1, 12)); }); @@ -493,8 +493,8 @@ suite('SnippetSession', function () { assertSelections(editor, new Selection(1, 16, 1, 16)); session.next(); - assert.equal(model.getValue(), 'clk : std_logic;\n'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), 'clk : std_logic;\n'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(2, 1, 2, 1)); }); @@ -532,8 +532,8 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: 'string' }); session.next(); - assert.equal(model.getValue(), expected); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), expected); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(4, 2, 4, 2)); }); @@ -556,8 +556,8 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: ' := \'1\'' }); session.next(); - assert.equal(model.getValue(), 'clk : std_logic := \'1\';\n'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), 'clk : std_logic := \'1\';\n'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(2, 1, 2, 1)); }); @@ -570,13 +570,13 @@ suite('SnippetSession', function () { assertSelections(editor, new Selection(1, 1, 1, 2), new Selection(1, 5, 1, 6)); session.next(); - assert.equal(model.getValue(), '{fff}'); + assert.strictEqual(model.getValue(), '{fff}'); assertSelections(editor, new Selection(1, 2, 1, 5)); editor.trigger('test', 'type', { text: 'ggg' }); session.next(); - assert.equal(model.getValue(), '{ggg}'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), '{ggg}'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 6, 1, 6)); }); @@ -584,7 +584,7 @@ suite('SnippetSession', function () { editor.getModel().setValue(''); const session = new SnippetSession(editor, '${1:{}${2:fff}${1/[\\{]/}/}$0'); session.insert(); - assert.equal(editor.getModel().getValue(), '{fff{'); + assert.strictEqual(editor.getModel().getValue(), '{fff{'); assertSelections(editor, new Selection(1, 1, 1, 2), new Selection(1, 5, 1, 6)); session.next(); @@ -599,25 +599,25 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: '1' }); editor.trigger('test', 'type', { text: '\n' }); - assert.equal(editor.getModel()!.getValue(), 'test 1\n'); + assert.strictEqual(editor.getModel()!.getValue(), 'test 1\n'); session.merge('test ${1:replaceme}'); editor.trigger('test', 'type', { text: '2' }); editor.trigger('test', 'type', { text: '\n' }); - assert.equal(editor.getModel()!.getValue(), 'test 1\ntest 2\n'); + assert.strictEqual(editor.getModel()!.getValue(), 'test 1\ntest 2\n'); session.merge('test ${1:replaceme}'); editor.trigger('test', 'type', { text: '3' }); editor.trigger('test', 'type', { text: '\n' }); - assert.equal(editor.getModel()!.getValue(), 'test 1\ntest 2\ntest 3\n'); + assert.strictEqual(editor.getModel()!.getValue(), 'test 1\ntest 2\ntest 3\n'); session.merge('test ${1:replaceme}'); editor.trigger('test', 'type', { text: '4' }); editor.trigger('test', 'type', { text: '\n' }); - assert.equal(editor.getModel()!.getValue(), 'test 1\ntest 2\ntest 3\ntest 4\n'); + assert.strictEqual(editor.getModel()!.getValue(), 'test 1\ntest 2\ntest 3\ntest 4\n'); }); test('Snippet variable text isn\'t whitespace normalised, #31124', function () { @@ -642,7 +642,7 @@ suite('SnippetSession', function () { 'end' ].join('\n'); - assert.equal(editor.getModel()!.getValue(), expected); + assert.strictEqual(editor.getModel()!.getValue(), expected); editor.getModel()!.setValue([ 'start', @@ -665,7 +665,7 @@ suite('SnippetSession', function () { 'end' ].join('\n'); - assert.equal(editor.getModel()!.getValue(), expected); + assert.strictEqual(editor.getModel()!.getValue(), expected); }); test('Selecting text from left to right, and choosing item messes up code, #31199', function () { @@ -680,7 +680,7 @@ suite('SnippetSession', function () { editor.setSelections([new Selection(1, 9, 1, 12)]); new SnippetSession(editor, 'far', { overwriteBefore: 3, overwriteAfter: 0, adjustWhitespace: true, clipboardText: undefined, overtypingCapturer: undefined }).insert(); - assert.equal(model.getValue(), 'console.far'); + assert.strictEqual(model.getValue(), 'console.far'); }); test('Tabs don\'t get replaced with spaces in snippet transformations #103818', function () { diff --git a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts index 8e939a5ac..dff2a64be 100644 --- a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts @@ -9,11 +9,14 @@ import { Selection } from 'vs/editor/common/core/selection'; import { SelectionBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, ClipboardBasedVariableResolver, TimeBasedVariableResolver, WorkspaceBasedVariableResolver } from 'vs/editor/contrib/snippet/snippetVariables'; import { SnippetParser, Variable, VariableResolver } from 'vs/editor/contrib/snippet/snippetParser'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { toWorkspaceFolders, IWorkspace, IWorkspaceContextService, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IWorkspace, IWorkspaceContextService, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ILabelService } from 'vs/platform/label/common/label'; import { mock } from 'vs/base/test/common/mock'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { sep } from 'vs/base/common/path'; +import { toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; suite('Snippet Variables Resolver', function () { @@ -48,9 +51,9 @@ suite('Snippet Variables Resolver', function () { const variable = snippet.children[0]; variable.resolve(resolver); if (variable.children.length === 0) { - assert.equal(undefined, expected); + assert.strictEqual(undefined, expected); } else { - assert.equal(variable.toString(), expected); + assert.strictEqual(variable.toString(), expected); } } @@ -127,17 +130,17 @@ suite('Snippet Variables Resolver', function () { test('TextmateSnippet, resolve variable', function () { const snippet = new SnippetParser().parse('"$TM_CURRENT_WORD"', true); - assert.equal(snippet.toString(), '""'); + assert.strictEqual(snippet.toString(), '""'); snippet.resolveVariables(resolver); - assert.equal(snippet.toString(), '"this"'); + assert.strictEqual(snippet.toString(), '"this"'); }); test('TextmateSnippet, resolve variable with default', function () { const snippet = new SnippetParser().parse('"${TM_CURRENT_WORD:foo}"', true); - assert.equal(snippet.toString(), '"foo"'); + assert.strictEqual(snippet.toString(), '"foo"'); snippet.resolveVariables(resolver); - assert.equal(snippet.toString(), '"this"'); + assert.strictEqual(snippet.toString(), '"this"'); }); test('More useful environment variables for snippets, #32737', function () { @@ -169,14 +172,14 @@ suite('Snippet Variables Resolver', function () { .resolveVariables({ resolve(variable) { return varValue || variable.name; } }); const actual = snippet.toString(); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); } test('Variable Snippet Transform', function () { const snippet = new SnippetParser().parse('name=${TM_FILENAME/(.*)\\..+$/$1/}', true); snippet.resolveVariables(resolver); - assert.equal(snippet.toString(), 'name=text'); + assert.strictEqual(snippet.toString(), 'name=text'); assertVariableResolve2('${ThisIsAVar/([A-Z]).*(Var)/$2/}', 'Var'); assertVariableResolve2('${ThisIsAVar/([A-Z]).*(Var)/$2-${1:/downcase}/}', 'Var-t'); @@ -267,7 +270,7 @@ suite('Snippet Variables Resolver', function () { const snippet = new SnippetParser().parse(`$${varName}`); const variable = snippet.children[0]; - assert.equal(variable.resolve(resolver), true, `${varName} failed to resolve`); + assert.strictEqual(variable.resolve(resolver), true, `${varName} failed to resolve`); } test('Add time variables for snippets #41631, #43140', function () { @@ -292,10 +295,10 @@ suite('Snippet Variables Resolver', function () { const snippet = new SnippetParser().parse('${TM_LINE_NUMBER/(10)/${1:?It is:It is not}/} line 10', true); snippet.resolveVariables({ resolve() { return '10'; } }); - assert.equal(snippet.toString(), 'It is line 10'); + assert.strictEqual(snippet.toString(), 'It is line 10'); snippet.resolveVariables({ resolve() { return '11'; } }); - assert.equal(snippet.toString(), 'It is not line 10'); + assert.strictEqual(snippet.toString(), 'It is not line 10'); }); test('Add workspace name and folder variables for snippets #68261', function () { @@ -332,10 +335,55 @@ suite('Snippet Variables Resolver', function () { // workspace with config const workspaceConfigPath = URI.file('testWorkspace.code-workspace'); - workspace = new Workspace('', toWorkspaceFolders([{ path: 'folderName' }], workspaceConfigPath), workspaceConfigPath); + workspace = new Workspace('', toWorkspaceFolders([{ path: 'folderName' }], workspaceConfigPath, extUriBiasedIgnorePathCase), workspaceConfigPath); assertVariableResolve(resolver, 'WORKSPACE_NAME', 'testWorkspace'); if (!isWindows) { assertVariableResolve(resolver, 'WORKSPACE_FOLDER', '/'); } }); + + test('Add RELATIVE_FILEPATH snippet variable #114208', function () { + + let resolver: VariableResolver; + + // Mock a label service (only coded for file uris) + const workspaceLabelService = ((rootPath: string): ILabelService => { + const labelService = new class extends mock() { + getUriLabel(uri: URI, options: { relative?: boolean } = {}) { + const rootFsPath = URI.file(rootPath).fsPath + sep; + const fsPath = uri.fsPath; + if (options.relative && rootPath && fsPath.startsWith(rootFsPath)) { + return fsPath.substring(rootFsPath.length); + } + return fsPath; + } + }; + return labelService; + }); + + const model = createTextModel('', undefined, undefined, URI.parse('file:///foo/files/text.txt')); + + // empty workspace + resolver = new ModelBasedVariableResolver( + workspaceLabelService(''), + model + ); + + if (!isWindows) { + assertVariableResolve(resolver, 'RELATIVE_FILEPATH', '/foo/files/text.txt'); + } else { + assertVariableResolve(resolver, 'RELATIVE_FILEPATH', '\\foo\\files\\text.txt'); + } + + // single folder workspace + resolver = new ModelBasedVariableResolver( + workspaceLabelService('/foo'), + model + ); + if (!isWindows) { + assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files/text.txt'); + } else { + assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files\\text.txt'); + } + }); }); diff --git a/src/vs/editor/contrib/suggest/suggestModel.ts b/src/vs/editor/contrib/suggest/suggestModel.ts index 9a88e1ccc..747f78a4b 100644 --- a/src/vs/editor/contrib/suggest/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/suggestModel.ts @@ -443,10 +443,6 @@ export class SuggestModel implements IDisposable { this._requestToken?.dispose(); - if (this._state === State.Idle) { - return; - } - if (!this._editor.hasModel()) { return; } @@ -456,6 +452,10 @@ export class SuggestModel implements IDisposable { clipboardText = await this._clipboardService.readText(); } + if (this._state === State.Idle) { + return; + } + const model = this._editor.getModel(); let items = completions.items; diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 02f6233f2..158e38015 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -5,7 +5,6 @@ import 'vs/css!./media/suggest'; import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded -import 'vs/editor/contrib/documentSymbols/outlineTree'; // The codicon symbol colors are defined here and must be loaded import * as nls from 'vs/nls'; import * as strings from 'vs/base/common/strings'; import * as dom from 'vs/base/browser/dom'; @@ -436,6 +435,7 @@ export class SuggestWidget implements IDisposable { case State.Hidden: dom.hide(this._messageElement, this._listElement, this._status.element); this._details.hide(true); + this._status.hide(); this._contentWidget.hide(); this._ctxSuggestWidgetVisible.reset(); this._ctxSuggestWidgetMultipleSuggestions.reset(); @@ -483,6 +483,7 @@ export class SuggestWidget implements IDisposable { } private _show(): void { + this._status.show(); this._contentWidget.show(); this._layout(this._persistedSize.restore()); this._ctxSuggestWidgetVisible.set(true); @@ -702,6 +703,14 @@ export class SuggestWidget implements IDisposable { this._loadingTimeout?.dispose(); this._setState(State.Hidden); this._onDidHide.fire(this); + + // ensure that a reasonable widget height is persisted so that + // accidential "resize-to-single-items" cases aren't happening + const dim = this._persistedSize.restore(); + const minPersistedHeight = Math.ceil(this.getLayoutInfo().itemHeight * 4.3); + if (dim && dim.height < minPersistedHeight) { + this._persistedSize.store(dim.with(undefined, minPersistedHeight)); + } } isFrozen(): boolean { @@ -734,12 +743,16 @@ export class SuggestWidget implements IDisposable { return; } - let height = size?.height; - let width = size?.width; - const bodyBox = dom.getClientArea(document.body); const info = this.getLayoutInfo(); + if (!size) { + size = info.defaultSize; + } + + let height = size.height; + let width = size.width; + // status bar this._status.element.style.lineHeight = `${info.itemHeight}px`; @@ -756,9 +769,6 @@ export class SuggestWidget implements IDisposable { // width math const maxWidth = bodyBox.width - info.borderHeight - 2 * info.horizontalPadding; - if (width === undefined) { - width = info.defaultSize.width; - } if (width > maxWidth) { width = maxWidth; } @@ -766,7 +776,6 @@ export class SuggestWidget implements IDisposable { // height math const fullHeight = info.statusBarHeight + this._list.contentHeight + info.borderHeight; - const preferredHeight = info.defaultSize.height; const minHeight = info.itemHeight + info.statusBarHeight; const editorBox = dom.getDomNodePagePosition(this.editor.getDomNode()); const cursorBox = this.editor.getScrolledVisiblePosition(this.editor.getPosition()); @@ -775,15 +784,12 @@ export class SuggestWidget implements IDisposable { const maxHeightAbove = Math.min(editorBox.top + cursorBox.top - info.verticalPadding, fullHeight); let maxHeight = Math.min(Math.max(maxHeightAbove, maxHeightBelow) + info.borderHeight, fullHeight); - if (height && height === this._cappedHeight?.capped) { + if (height === this._cappedHeight?.capped) { // Restore the old (wanted) height when the current // height is capped to fit height = this._cappedHeight.wanted; } - if (height === undefined) { - height = Math.min(preferredHeight, fullHeight); - } if (height < minHeight) { height = minHeight; } @@ -801,14 +807,14 @@ export class SuggestWidget implements IDisposable { this.element.enableSashes(false, true, true, false); maxHeight = maxHeightBelow; } - this.element.preferredSize = new dom.Dimension(preferredWidth, preferredHeight); + this.element.preferredSize = new dom.Dimension(preferredWidth, info.defaultSize.height); this.element.maxSize = new dom.Dimension(maxWidth, maxHeight); this.element.minSize = new dom.Dimension(220, minHeight); // Know when the height was capped to fit and remember // the wanted height for later. This is required when going // left to widen suggestions. - this._cappedHeight = size && height === fullHeight + this._cappedHeight = height === fullHeight ? { wanted: this._cappedHeight?.wanted ?? size.height, capped: height } : undefined; } diff --git a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts index 26cdc77ae..71098fe06 100644 --- a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts +++ b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts @@ -117,7 +117,7 @@ export class ItemRenderer implements IListRenderer(action => { - return action instanceof MenuItemAction - ? instantiationService.createInstance(StatusBarViewItem, action) - : undefined; + return action instanceof MenuItemAction ? instantiationService.createInstance(StatusBarViewItem, action) : undefined; }); - const leftActions = new ActionBar(this.element, { actionViewItemProvider }); - const rightActions = new ActionBar(this.element, { actionViewItemProvider }); - const menu = menuService.createMenu(suggestWidgetStatusbarMenu, contextKeyService); + this._leftActions = new ActionBar(this.element, { actionViewItemProvider }); + this._rightActions = new ActionBar(this.element, { actionViewItemProvider }); - leftActions.domNode.classList.add('left'); - rightActions.domNode.classList.add('right'); + this._leftActions.domNode.classList.add('left'); + this._rightActions.domNode.classList.add('right'); + } + dispose(): void { + this._menuDisposables.dispose(); + this.element.remove(); + } + + show(): void { + const menu = this._menuService.createMenu(suggestWidgetStatusbarMenu, this._contextKeyService); const renderMenu = () => { const left: IAction[] = []; const right: IAction[] = []; @@ -68,17 +75,16 @@ export class SuggestWidgetStatus { right.push(...actions); } } - leftActions.clear(); - leftActions.push(left); - rightActions.clear(); - rightActions.push(right); + this._leftActions.clear(); + this._leftActions.push(left); + this._rightActions.clear(); + this._rightActions.push(right); }; - this._disposables.add(menu.onDidChange(() => renderMenu())); - this._disposables.add(menu); + this._menuDisposables.add(menu.onDidChange(() => renderMenu())); + this._menuDisposables.add(menu); } - dispose(): void { - this._disposables.dispose(); - this.element.remove(); + hide(): void { + this._menuDisposables.clear(); } } diff --git a/src/vs/editor/contrib/suggest/test/suggestController.test.ts b/src/vs/editor/contrib/suggest/test/suggestController.test.ts index 8644d4498..491a75739 100644 --- a/src/vs/editor/contrib/suggest/test/suggestController.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestController.test.ts @@ -57,6 +57,7 @@ suite('SuggestController', function () { createMenu() { return new class extends mock() { onDidChange = Event.None; + dispose() { } }; } }] diff --git a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts index 3193c6787..ce99f9758 100644 --- a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts @@ -71,7 +71,7 @@ suite('SuggestModel - Context', function () { this._register(TokenizationRegistry.register(this.getLanguageIdentifier().language, { getInitialState: (): IState => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { const tokensArr: number[] = []; let prevLanguageId: LanguageIdentifier | undefined = undefined; for (let i = 0; i < line.length; i++) { diff --git a/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts b/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts index 44ca58390..43fdcc903 100644 --- a/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts +++ b/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts @@ -16,6 +16,7 @@ import { toMultilineTokens2, SemanticTokensProviderStyling } from 'vs/editor/com import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { isSemanticColoringEnabled, SEMANTIC_HIGHLIGHTING_SETTING_ID } from 'vs/editor/common/services/modelServiceImpl'; +import { getDocumentRangeSemanticTokensProvider } from 'vs/editor/common/services/getSemanticTokens'; class ViewportSemanticTokensContribution extends Disposable implements IEditorContribution { @@ -66,11 +67,6 @@ class ViewportSemanticTokensContribution extends Disposable implements IEditorCo })); } - private static _getSemanticColoringProvider(model: ITextModel): DocumentRangeSemanticTokensProvider | null { - const result = DocumentRangeSemanticTokensProviderRegistry.ordered(model); - return (result.length > 0 ? result[0] : null); - } - private _cancelAll(): void { for (const request of this._outstandingRequests) { request.cancel(); @@ -101,7 +97,7 @@ class ViewportSemanticTokensContribution extends Disposable implements IEditorCo } return; } - const provider = ViewportSemanticTokensContribution._getSemanticColoringProvider(model); + const provider = getDocumentRangeSemanticTokensProvider(model); if (!provider) { if (model.hasSomeSemanticTokens()) { model.setSemanticTokens(null, false); diff --git a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts index 49433e1f6..ee2eb0738 100644 --- a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts @@ -110,7 +110,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordLeft - with selection', () => { @@ -123,7 +123,7 @@ suite('WordOperations', () => { ], {}, (editor) => { editor.setPosition(new Position(5, 2)); cursorWordLeft(editor, true); - assert.deepEqual(editor.getSelection(), new Selection(5, 2, 5, 1)); + assert.deepStrictEqual(editor.getSelection(), new Selection(5, 2, 5, 1)); }); }); @@ -138,7 +138,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordLeft - issue #48046: Word selection doesn\'t work as usual', () => { @@ -154,7 +154,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordLeftSelect - issue #74369: cursorWordLeft and cursorWordLeftSelect do not behave consistently', () => { @@ -170,7 +170,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordStartLeft', () => { @@ -185,7 +185,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordStartLeft - issue #51119: regression makes VS compatibility impossible', () => { @@ -200,7 +200,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #51275 - cursorWordStartLeft does not push undo/redo stack element', () => { @@ -212,16 +212,16 @@ suite('WordOperations', () => { withTestCodeEditor('', {}, (editor, viewModel) => { type(viewModel, 'foo bar baz'); - assert.equal(editor.getValue(), 'foo bar baz'); + assert.strictEqual(editor.getValue(), 'foo bar baz'); cursorWordStartLeft(editor); cursorWordStartLeft(editor); type(viewModel, 'q'); - assert.equal(editor.getValue(), 'foo qbar baz'); + assert.strictEqual(editor.getValue(), 'foo qbar baz'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(editor.getValue(), 'foo bar baz'); + assert.strictEqual(editor.getValue(), 'foo bar baz'); }); }); @@ -236,7 +236,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordRight - simple', () => { @@ -256,7 +256,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(5, 2)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordRight - selection', () => { @@ -269,7 +269,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { editor.setPosition(new Position(1, 1)); cursorWordRight(editor, true); - assert.deepEqual(editor.getSelection(), new Selection(1, 1, 1, 8)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 1, 8)); }); }); @@ -286,7 +286,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 50)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordRight - issue #41199', () => { @@ -302,7 +302,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 17)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('moveWordEndRight', () => { @@ -318,7 +318,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 50)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('moveWordStartRight', () => { @@ -335,7 +335,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 50)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #51119: cursorWordStartRight regression makes VS compatibility impossible', () => { @@ -350,7 +350,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 15)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #64810: cursorWordStartRight skips first word after newline', () => { @@ -365,7 +365,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(2, 12)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordAccessibilityLeft', () => { @@ -379,7 +379,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordAccessibilityRight', () => { @@ -393,7 +393,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 50)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordLeft for non-empty selection', () => { @@ -407,8 +407,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setSelection(new Selection(3, 7, 3, 9)); deleteWordLeft(editor); - assert.equal(model.getLineContent(3), ' Thd Line🐶'); - assert.deepEqual(editor.getPosition(), new Position(3, 7)); + assert.strictEqual(model.getLineContent(3), ' Thd Line🐶'); + assert.deepStrictEqual(editor.getPosition(), new Position(3, 7)); }); }); @@ -423,8 +423,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 1)); deleteWordLeft(editor); - assert.equal(model.getLineContent(1), ' \tMy First Line\t '); - assert.deepEqual(editor.getPosition(), new Position(1, 1)); + assert.strictEqual(model.getLineContent(1), ' \tMy First Line\t '); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 1)); }); }); @@ -439,8 +439,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(3, 11)); deleteWordLeft(editor); - assert.equal(model.getLineContent(3), ' Line🐶'); - assert.deepEqual(editor.getPosition(), new Position(3, 5)); + assert.strictEqual(model.getLineContent(3), ' Line🐶'); + assert.deepStrictEqual(editor.getPosition(), new Position(3, 5)); }); }); @@ -455,8 +455,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(2, 11)); deleteWordLeft(editor); - assert.equal(model.getLineContent(2), '\tMy Line'); - assert.deepEqual(editor.getPosition(), new Position(2, 5)); + assert.strictEqual(model.getLineContent(2), '\tMy Line'); + assert.deepStrictEqual(editor.getPosition(), new Position(2, 5)); }); }); @@ -471,8 +471,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 12)); deleteWordLeft(editor); - assert.equal(model.getLineContent(1), ' \tMy st Line\t '); - assert.deepEqual(editor.getPosition(), new Position(1, 9)); + assert.strictEqual(model.getLineContent(1), ' \tMy st Line\t '); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); }); }); @@ -487,8 +487,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setSelection(new Selection(3, 7, 3, 9)); deleteWordRight(editor); - assert.equal(model.getLineContent(3), ' Thd Line🐶'); - assert.deepEqual(editor.getPosition(), new Position(3, 7)); + assert.strictEqual(model.getLineContent(3), ' Thd Line🐶'); + assert.deepStrictEqual(editor.getPosition(), new Position(3, 7)); }); }); @@ -503,8 +503,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(5, 3)); deleteWordRight(editor); - assert.equal(model.getLineContent(5), '1'); - assert.deepEqual(editor.getPosition(), new Position(5, 2)); + assert.strictEqual(model.getLineContent(5), '1'); + assert.deepStrictEqual(editor.getPosition(), new Position(5, 2)); }); }); @@ -519,8 +519,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(3, 1)); deleteWordRight(editor); - assert.equal(model.getLineContent(3), 'Third Line🐶'); - assert.deepEqual(editor.getPosition(), new Position(3, 1)); + assert.strictEqual(model.getLineContent(3), 'Third Line🐶'); + assert.deepStrictEqual(editor.getPosition(), new Position(3, 1)); }); }); @@ -535,8 +535,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(2, 5)); deleteWordRight(editor); - assert.equal(model.getLineContent(2), '\tMy Line'); - assert.deepEqual(editor.getPosition(), new Position(2, 5)); + assert.strictEqual(model.getLineContent(2), '\tMy Line'); + assert.deepStrictEqual(editor.getPosition(), new Position(2, 5)); }); }); @@ -551,8 +551,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 11)); deleteWordRight(editor); - assert.equal(model.getLineContent(1), ' \tMy Fi Line\t '); - assert.deepEqual(editor.getPosition(), new Position(1, 11)); + assert.strictEqual(model.getLineContent(1), ' \tMy Fi Line\t '); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 11)); }); }); @@ -569,7 +569,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordStartLeft', () => { @@ -585,7 +585,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordEndLeft', () => { @@ -601,7 +601,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordLeft - issue #24947', () => { @@ -611,7 +611,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), '{}'); + deleteWordLeft(editor); assert.strictEqual(model.getLineContent(1), '{}'); }); withTestCodeEditor([ @@ -620,7 +620,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); - deleteWordStartLeft(editor); assert.equal(model.getLineContent(1), '{}'); + deleteWordStartLeft(editor); assert.strictEqual(model.getLineContent(1), '{}'); }); withTestCodeEditor([ @@ -629,7 +629,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); - deleteWordEndLeft(editor); assert.equal(model.getLineContent(1), '{}'); + deleteWordEndLeft(editor); assert.strictEqual(model.getLineContent(1), '{}'); }); }); @@ -644,7 +644,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordRight - issue #3882', () => { @@ -654,7 +654,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(1, 24)); - deleteWordRight(editor); assert.equal(model.getLineContent(1), 'public void Add( int x,int y )', '001'); + deleteWordRight(editor); assert.strictEqual(model.getLineContent(1), 'public void Add( int x,int y )', '001'); }); }); @@ -665,7 +665,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(1, 24)); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), 'public void Add( int x,int y )', '001'); + deleteWordStartRight(editor); assert.strictEqual(model.getLineContent(1), 'public void Add( int x,int y )', '001'); }); }); @@ -676,7 +676,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(1, 24)); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), 'public void Add( int x,int y )', '001'); + deleteWordEndRight(editor); assert.strictEqual(model.getLineContent(1), 'public void Add( int x,int y )', '001'); }); }); @@ -691,7 +691,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordEndRight', () => { @@ -705,7 +705,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordRight - issue #3882 (1): Ctrl+Delete removing entire line when used at the end of line', () => { @@ -715,7 +715,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(1, 18)); - deleteWordRight(editor); assert.equal(model.getLineContent(1), 'A line with text.And another one', '001'); + deleteWordRight(editor); assert.strictEqual(model.getLineContent(1), 'A line with text.And another one', '001'); }); }); @@ -726,7 +726,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), 'A line with text. And another one', '001'); + deleteWordLeft(editor); assert.strictEqual(model.getLineContent(1), 'A line with text. And another one', '001'); }); }); @@ -748,7 +748,7 @@ suite('WordOperations', () => { withTestCodeEditor(null, { model }, (editor, _) => { editor.setPosition(new Position(1, 4)); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), 'a '); + deleteWordLeft(editor); assert.strictEqual(model.getLineContent(1), 'a '); }); model.dispose(); @@ -764,7 +764,7 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'Line1\nLine2'); + assert.strictEqual(model.getValue(), 'Line1\nLine2'); }); }); @@ -775,7 +775,7 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 6)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'Justsome text.'); + assert.strictEqual(model.getValue(), 'Justsome text.'); }); }); @@ -786,7 +786,7 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 6)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'Justsome text.'); + assert.strictEqual(model.getValue(), 'Justsome text.'); }); }); @@ -797,19 +797,19 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 6)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'Just"some text.'); + assert.strictEqual(model.getValue(), 'Just"some text.'); deleteInsideWord(editor); - assert.equal(model.getValue(), '"some text.'); + assert.strictEqual(model.getValue(), '"some text.'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'some text.'); + assert.strictEqual(model.getValue(), 'some text.'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'text.'); + assert.strictEqual(model.getValue(), 'text.'); deleteInsideWord(editor); - assert.equal(model.getValue(), '.'); + assert.strictEqual(model.getValue(), '.'); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); }); }); @@ -820,19 +820,19 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 7)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x=3+45+6'); + assert.strictEqual(model.getValue(), 'x=3+45+6'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x=3++6'); + assert.strictEqual(model.getValue(), 'x=3++6'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x=36'); + assert.strictEqual(model.getValue(), 'x=36'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x='); + assert.strictEqual(model.getValue(), 'x='); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x'); + assert.strictEqual(model.getValue(), 'x'); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); }); }); @@ -843,13 +843,13 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 7)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'This interesting'); + assert.strictEqual(model.getValue(), 'This interesting'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'This'); + assert.strictEqual(model.getValue(), 'This'); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); }); }); @@ -860,13 +860,13 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 7)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'This interesting'); + assert.strictEqual(model.getValue(), 'This interesting'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'This'); + assert.strictEqual(model.getValue(), 'This'); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); }); }); }); diff --git a/src/vs/editor/contrib/wordOperations/wordOperations.ts b/src/vs/editor/contrib/wordOperations/wordOperations.ts index 82dcbddc5..ac3f987de 100644 --- a/src/vs/editor/contrib/wordOperations/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/wordOperations.ts @@ -55,7 +55,7 @@ export abstract class MoveWordCommand extends EditorCommand { }); model.pushStackElement(); - editor._getViewModel().setCursorStates('moveWordCommand', CursorChangeReason.NotSet, result.map(r => CursorState.fromModelSelection(r))); + editor._getViewModel().setCursorStates('moveWordCommand', CursorChangeReason.Explicit, result.map(r => CursorState.fromModelSelection(r))); if (result.length === 1) { const pos = new Position(result[0].positionLineNumber, result[0].positionColumn); editor.revealPosition(pos, ScrollType.Smooth); diff --git a/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts b/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts index 70e9c87c6..446711f2a 100644 --- a/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts +++ b/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts @@ -49,7 +49,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartLeft - issue #53899: whitespace', () => { @@ -63,7 +63,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartLeft - issue #53899: underscores', () => { @@ -77,7 +77,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartRight - basic', () => { @@ -95,7 +95,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(3, 9)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartRight - issue #53899: whitespace', () => { @@ -109,7 +109,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 52)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartRight - issue #53899: underscores', () => { @@ -123,7 +123,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 52)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartRight - issue #53899: second case', () => { @@ -142,7 +142,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(4, 7)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #93239 - cursorWordPartRight', () => { @@ -158,7 +158,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 8)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #93239 - cursorWordPartLeft', () => { @@ -174,7 +174,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordPartLeft - basic', () => { @@ -188,7 +188,7 @@ suite('WordPartOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordPartRight - basic', () => { @@ -202,6 +202,6 @@ suite('WordPartOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); }); diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 904283213..9b4870b45 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -23,12 +23,13 @@ import 'vs/editor/contrib/find/findController'; import 'vs/editor/contrib/folding/folding'; import 'vs/editor/contrib/fontZoom/fontZoom'; import 'vs/editor/contrib/format/formatActions'; -import 'vs/editor/contrib/gotoSymbol/documentSymbols'; +import 'vs/editor/contrib/documentSymbols/documentSymbols'; import 'vs/editor/contrib/gotoSymbol/goToCommands'; import 'vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition'; import 'vs/editor/contrib/gotoError/gotoError'; import 'vs/editor/contrib/hover/hover'; import 'vs/editor/contrib/indentation/indentation'; +import 'vs/editor/contrib/inlineHints/inlineHintsController'; import 'vs/editor/contrib/inPlaceReplace/inPlaceReplace'; import 'vs/editor/contrib/linesOperations/linesOperations'; import 'vs/editor/contrib/linkedEditing/linkedEditing'; diff --git a/src/vs/editor/editor.api.ts b/src/vs/editor/editor.api.ts index 0e4c48c34..6b2cc6c04 100644 --- a/src/vs/editor/editor.api.ts +++ b/src/vs/editor/editor.api.ts @@ -7,6 +7,8 @@ import { EditorOptions, WrappingIndent, EditorAutoIndentStrategy } from 'vs/edit import { createMonacoBaseAPI } from 'vs/editor/common/standalone/standaloneBase'; import { createMonacoEditorAPI } from 'vs/editor/standalone/browser/standaloneEditor'; import { createMonacoLanguagesAPI } from 'vs/editor/standalone/browser/standaloneLanguages'; +import { globals } from 'vs/base/common/platform'; +import { FormattingConflicts } from 'vs/editor/contrib/format/format'; // Set defaults for standalone editor EditorOptions.wrappingIndent.defaultValue = WrappingIndent.None; @@ -14,6 +16,10 @@ EditorOptions.glyphMargin.defaultValue = false; EditorOptions.autoIndent.defaultValue = EditorAutoIndentStrategy.Advanced; EditorOptions.overviewRulerLanes.defaultValue = 2; +// We need to register a formatter selector which simply picks the first available formatter. +// See https://github.com/microsoft/monaco-editor/issues/2327 +FormattingConflicts.setFormatterSelector((formatter, document, mode) => Promise.resolve(formatter[0])); + const api = createMonacoBaseAPI(); api.editor = createMonacoEditorAPI(); api.languages = createMonacoLanguagesAPI(); @@ -32,7 +38,9 @@ export const Token = api.Token; export const editor = api.editor; export const languages = api.languages; -self.monaco = api; +if (globals.MonacoEnvironment?.globalAPI || globals.define?.amd) { + self.monaco = api; +} if (typeof self.require !== 'undefined' && typeof self.require.config === 'function') { self.require.config({ diff --git a/src/vs/editor/standalone/browser/colorizer.ts b/src/vs/editor/standalone/browser/colorizer.ts index 22bad13e9..f9d335696 100644 --- a/src/vs/editor/standalone/browser/colorizer.ts +++ b/src/vs/editor/standalone/browser/colorizer.ts @@ -42,8 +42,8 @@ export class Colorizer { let text = domNode.firstChild ? domNode.firstChild.nodeValue : ''; domNode.className += ' ' + theme; let render = (str: string) => { - const trustedhtml = ttPolicy ? ttPolicy.createHTML(str) : str; - domNode.innerHTML = trustedhtml as unknown as string; + const trustedhtml = ttPolicy?.createHTML(str) ?? str; + domNode.innerHTML = trustedhtml as string; }; return this.colorize(modeService, text || '', mimeType, options).then(render, (err) => console.error(err)); } @@ -222,7 +222,7 @@ function _actualColorize(lines: string[], tabSize: number, tokenizationSupport: for (let i = 0, length = lines.length; i < length; i++) { let line = lines[i]; - let tokenizeResult = tokenizationSupport.tokenize2(line, state, 0); + let tokenizeResult = tokenizationSupport.tokenize2(line, true, state, 0); LineTokens.convertToEndOffset(tokenizeResult.tokens, line.length); let lineTokens = new LineTokens(tokenizeResult.tokens, line); const isBasicASCII = ViewLineRenderingData.isBasicASCII(line, /* check for basic ASCII */true); diff --git a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts index 192f92185..88b54c867 100644 --- a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts +++ b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts @@ -137,8 +137,8 @@ function getSafeTokenizationSupport(languageIdentifier: LanguageIdentifier): ITo } return { getInitialState: () => NULL_STATE, - tokenize: (line: string, state: IState, deltaOffset: number) => nullTokenize(languageIdentifier.language, line, state, deltaOffset), - tokenize2: (line: string, state: IState, deltaOffset: number) => nullTokenize2(languageIdentifier.id, line, state, deltaOffset) + tokenize: (line: string, hasEOL: boolean, state: IState, deltaOffset: number) => nullTokenize(languageIdentifier.language, line, state, deltaOffset), + tokenize2: (line: string, hasEOL: boolean, state: IState, deltaOffset: number) => nullTokenize2(languageIdentifier.id, line, state, deltaOffset) }; } @@ -293,8 +293,8 @@ class InspectTokensWidget extends Disposable implements IContentWidget { private _getTokensAtLine(lineNumber: number): ICompleteLineTokenization { let stateBeforeLine = this._getStateBeforeLine(lineNumber); - let tokenizationResult1 = this._tokenizationSupport.tokenize(this._model.getLineContent(lineNumber), stateBeforeLine, 0); - let tokenizationResult2 = this._tokenizationSupport.tokenize2(this._model.getLineContent(lineNumber), stateBeforeLine, 0); + let tokenizationResult1 = this._tokenizationSupport.tokenize(this._model.getLineContent(lineNumber), true, stateBeforeLine, 0); + let tokenizationResult2 = this._tokenizationSupport.tokenize2(this._model.getLineContent(lineNumber), true, stateBeforeLine, 0); return { startState: stateBeforeLine, @@ -308,7 +308,7 @@ class InspectTokensWidget extends Disposable implements IContentWidget { let state: IState = this._tokenizationSupport.getInitialState(); for (let i = 1; i < lineNumber; i++) { - let tokenizationResult = this._tokenizationSupport.tokenize(this._model.getLineContent(i), state, 0); + let tokenizationResult = this._tokenizationSupport.tokenize(this._model.getLineContent(i), true, state, 0); state = tokenizationResult.endState; } diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index ba51dfd9e..3410b5860 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -176,8 +176,8 @@ export class SimpleEditorProgressService implements IEditorProgressService { return SimpleEditorProgressService.NULL_PROGRESS_RUNNER; } - showWhile(promise: Promise, delay?: number): Promise { - return Promise.resolve(undefined); + async showWhile(promise: Promise, delay?: number): Promise { + await promise; } } @@ -638,12 +638,12 @@ export class SimpleWorkspaceContextService implements IWorkspaceContextService { return resource && resource.scheme === SimpleWorkspaceContextService.SCHEME; } - public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean { + public isCurrentWorkspace(workspaceIdOrFolder: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI): boolean { return true; } } -export function applyConfigurationValues(configurationService: IConfigurationService, source: any, isDiffEditor: boolean): void { +export function updateConfigurationService(configurationService: IConfigurationService, source: any, isDiffEditor: boolean): void { if (!source) { return; } @@ -736,7 +736,7 @@ export class SimpleUriLabelService implements ILabelService { return basename(resource); } - public getWorkspaceLabel(workspace: IWorkspaceIdentifier | URI | IWorkspace, options?: { verbose: boolean; }): string { + public getWorkspaceLabel(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | IWorkspace, options?: { verbose: boolean; }): string { return ''; } diff --git a/src/vs/editor/standalone/browser/standalone-tokens.css b/src/vs/editor/standalone/browser/standalone-tokens.css index e97572e05..174273006 100644 --- a/src/vs/editor/standalone/browser/standalone-tokens.css +++ b/src/vs/editor/standalone/browser/standalone-tokens.css @@ -23,6 +23,19 @@ margin: 0; } +/* +In certain cases, the default positioning of the aria container (left: -999em) can cause scrollbars to appear. +So here we try to avoid that by using a different technique. See https://stackoverflow.com/a/26032207 +*/ +.monaco-aria-container { + position: absolute !important; + height: 1px; + width: 1px; + left: inherit !important; + overflow: hidden; + clip: rect(1px, 1px, 1px, 1px); +} + /* The hc-black theme is already high contrast optimized */ .monaco-editor.hc-black { -ms-high-contrast-adjust: none; diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 9344ee801..4e3c939d2 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -14,7 +14,7 @@ import { InternalEditorAction } from 'vs/editor/common/editorAction'; import { IModelChangedEvent } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { StandaloneKeybindingService, applyConfigurationValues } from 'vs/editor/standalone/browser/simpleServices'; +import { StandaloneKeybindingService, updateConfigurationService } from 'vs/editor/standalone/browser/simpleServices'; import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService'; import { IMenuItem, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; @@ -31,6 +31,9 @@ import { StandaloneCodeEditorNLS } from 'vs/editor/common/standaloneStrings'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { StandaloneThemeServiceImpl } from 'vs/editor/standalone/browser/standaloneThemeServiceImpl'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ILanguageSelection, IModeService } from 'vs/editor/common/services/modeService'; +import { URI } from 'vs/base/common/uri'; /** * Description of an action contribution @@ -227,7 +230,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon constructor( domElement: HTMLElement, - options: IStandaloneEditorConstructionOptions, + _options: Readonly, @IInstantiationService instantiationService: IInstantiationService, @ICodeEditorService codeEditorService: ICodeEditorService, @ICommandService commandService: ICommandService, @@ -237,7 +240,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon @INotificationService notificationService: INotificationService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - options = options || {}; + const options = { ..._options }; options.ariaLabel = options.ariaLabel || StandaloneCodeEditorNLS.editorViewAccessibleLabel; options.ariaLabel = options.ariaLabel + ';' + (StandaloneCodeEditorNLS.accessibilityHelpMessage); super(domElement, options, {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService); @@ -353,7 +356,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon constructor( domElement: HTMLElement, - options: IStandaloneEditorConstructionOptions | undefined, + _options: Readonly | undefined, toDispose: IDisposable, @IInstantiationService instantiationService: IInstantiationService, @ICodeEditorService codeEditorService: ICodeEditorService, @@ -364,11 +367,13 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon @IStandaloneThemeService themeService: IStandaloneThemeService, @INotificationService notificationService: INotificationService, @IConfigurationService configurationService: IConfigurationService, - @IAccessibilityService accessibilityService: IAccessibilityService + @IAccessibilityService accessibilityService: IAccessibilityService, + @IModelService modelService: IModelService, + @IModeService modeService: IModeService, ) { - applyConfigurationValues(configurationService, options, false); + const options = { ..._options }; + updateConfigurationService(configurationService, options, false); const themeDomRegistration = (themeService).registerEditorContainer(domElement); - options = options || {}; if (typeof options.theme === 'string') { themeService.setTheme(options.theme); } @@ -384,7 +389,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon let model: ITextModel | null; if (typeof _model === 'undefined') { - model = (self).monaco.editor.createModel(options.value || '', options.language || 'text/plain'); + model = createTextModel(modelService, modeService, options.value || '', options.language || 'text/plain', undefined); this._ownsModel = true; } else { model = _model; @@ -405,8 +410,8 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon super.dispose(); } - public updateOptions(newOptions: IEditorOptions & IGlobalEditorOptions): void { - applyConfigurationValues(this._configurationService, newOptions, false); + public updateOptions(newOptions: Readonly): void { + updateConfigurationService(this._configurationService, newOptions, false); if (typeof newOptions.theme === 'string') { this._standaloneThemeService.setTheme(newOptions.theme); } @@ -437,7 +442,7 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon constructor( domElement: HTMLElement, - options: IDiffEditorConstructionOptions | undefined, + _options: Readonly | undefined, toDispose: IDisposable, @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService contextKeyService: IContextKeyService, @@ -452,14 +457,14 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon @IEditorProgressService editorProgressService: IEditorProgressService, @IClipboardService clipboardService: IClipboardService, ) { - applyConfigurationValues(configurationService, options, true); + const options = { ..._options }; + updateConfigurationService(configurationService, options, true); const themeDomRegistration = (themeService).registerEditorContainer(domElement); - options = options || {}; if (typeof options.theme === 'string') { options.theme = themeService.setTheme(options.theme); } - super(domElement, options, clipboardService, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService); + super(domElement, options, {}, clipboardService, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService); this._contextViewService = contextViewService; this._configurationService = configurationService; @@ -475,15 +480,15 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon super.dispose(); } - public updateOptions(newOptions: IDiffEditorOptions & IGlobalEditorOptions): void { - applyConfigurationValues(this._configurationService, newOptions, true); + public updateOptions(newOptions: Readonly): void { + updateConfigurationService(this._configurationService, newOptions, true); if (typeof newOptions.theme === 'string') { this._standaloneThemeService.setTheme(newOptions.theme); } super.updateOptions(newOptions); } - protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: IEditorOptions): CodeEditorWidget { + protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: Readonly): CodeEditorWidget { return instantiationService.createInstance(StandaloneCodeEditor, container, options); } @@ -507,3 +512,26 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon return this.getModifiedEditor().addAction(descriptor); } } + +/** + * @internal + */ +export function createTextModel(modelService: IModelService, modeService: IModeService, value: string, language: string | undefined, uri: URI | undefined): ITextModel { + value = value || ''; + if (!language) { + const firstLF = value.indexOf('\n'); + let firstLine = value; + if (firstLF !== -1) { + firstLine = value.substring(0, firstLF); + } + return doCreateModel(modelService, value, modeService.createByFilepathOrFirstLine(uri || null, firstLine), uri); + } + return doCreateModel(modelService, value, modeService.create(language), uri); +} + +/** + * @internal + */ +function doCreateModel(modelService: IModelService, value: string, languageSelection: ILanguageSelection, uri: URI | undefined): ITextModel { + return modelService.createModel(value, languageSelection, uri); +} diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 6d5fc9c32..295f2cb68 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -18,16 +18,16 @@ import { FindMatch, ITextModel, TextModelResolvedOptions } from 'vs/editor/commo import * as modes from 'vs/editor/common/modes'; import { NULL_STATE, nullTokenize } from 'vs/editor/common/modes/nullMode'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { ILanguageSelection } from 'vs/editor/common/services/modeService'; +import { IModeService } from 'vs/editor/common/services/modeService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IWebWorkerOptions, MonacoWebWorker, createWebWorker as actualCreateWebWorker } from 'vs/editor/common/services/webWorker'; import * as standaloneEnums from 'vs/editor/common/standalone/standaloneEnums'; import { Colorizer, IColorizerElementOptions, IColorizerOptions } from 'vs/editor/standalone/browser/colorizer'; import { SimpleEditorModelResolverService } from 'vs/editor/standalone/browser/simpleServices'; -import { IDiffEditorConstructionOptions, IStandaloneEditorConstructionOptions, IStandaloneCodeEditor, IStandaloneDiffEditor, StandaloneDiffEditor, StandaloneEditor } from 'vs/editor/standalone/browser/standaloneCodeEditor'; +import { IDiffEditorConstructionOptions, IStandaloneEditorConstructionOptions, IStandaloneCodeEditor, IStandaloneDiffEditor, StandaloneDiffEditor, StandaloneEditor, createTextModel } from 'vs/editor/standalone/browser/standaloneCodeEditor'; import { DynamicStandaloneServices, IEditorOverrideServices, StaticServices } from 'vs/editor/standalone/browser/standaloneServices'; import { IStandaloneThemeData, IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -42,6 +42,7 @@ import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { StandaloneThemeServiceImpl } from 'vs/editor/standalone/browser/standaloneThemeServiceImpl'; import { splitLines } from 'vs/base/common/strings'; +import { IModelService } from 'vs/editor/common/services/modelService'; type Omit = Pick>; @@ -87,7 +88,9 @@ export function create(domElement: HTMLElement, options?: IStandaloneEditorConst services.get(IStandaloneThemeService), services.get(INotificationService), services.get(IConfigurationService), - services.get(IAccessibilityService) + services.get(IAccessibilityService), + services.get(IModelService), + services.get(IModeService), ); }); } @@ -140,27 +143,18 @@ export function createDiffNavigator(diffEditor: IStandaloneDiffEditor, opts?: ID return new DiffNavigator(diffEditor, opts); } -function doCreateModel(value: string, languageSelection: ILanguageSelection, uri?: URI): ITextModel { - return StaticServices.modelService.get().createModel(value, languageSelection, uri); -} - /** * Create a new editor model. * You can specify the language that should be set for this model or let the language be inferred from the `uri`. */ export function createModel(value: string, language?: string, uri?: URI): ITextModel { - value = value || ''; - - if (!language) { - let firstLF = value.indexOf('\n'); - let firstLine = value; - if (firstLF !== -1) { - firstLine = value.substring(0, firstLF); - } - - return doCreateModel(value, StaticServices.modeService.get().createByFilepathOrFirstLine(uri || null, firstLine), uri); - } - return doCreateModel(value, StaticServices.modeService.get().create(language), uri); + return createTextModel( + StaticServices.modelService.get(), + StaticServices.modeService.get(), + value, + language, + uri + ); } /** @@ -188,6 +182,14 @@ export function getModelMarkers(filter: { owner?: string, resource?: URI, take?: return StaticServices.markerService.get().read(filter); } +/** + * Emitted when markers change for a model. + * @event + */ +export function onDidChangeMarkers(listener: (e: readonly URI[]) => void): IDisposable { + return StaticServices.markerService.get().onMarkerChanged(listener); +} + /** * Get the model that has `uri` if it exists. */ @@ -276,7 +278,7 @@ function getSafeTokenizationSupport(language: string): Omit NULL_STATE, - tokenize: (line: string, state: modes.IState, deltaOffset: number) => nullTokenize(language, line, state, deltaOffset) + tokenize: (line: string, hasEOL: boolean, state: modes.IState, deltaOffset: number) => nullTokenize(language, line, state, deltaOffset) }; } @@ -294,7 +296,7 @@ export function tokenize(text: string, languageId: string): Token[][] { let state = tokenizationSupport.getInitialState(); for (let i = 0, len = lines.length; i < len; i++) { let line = lines[i]; - let tokenizationResult = tokenizationSupport.tokenize(line, state, 0); + let tokenizationResult = tokenizationSupport.tokenize(line, true, state, 0); result[i] = tokenizationResult.tokens; state = tokenizationResult.endState; @@ -323,6 +325,13 @@ export function remeasureFonts(): void { clearAllFontInfos(); } +/** + * Register a command. + */ +export function registerCommand(id: string, handler: (accessor: any, ...args: any[]) => void): IDisposable { + return CommandsRegistry.registerCommand({ id, handler }); +} + /** * @internal */ @@ -338,6 +347,7 @@ export function createMonacoEditorAPI(): typeof monaco.editor { setModelLanguage: setModelLanguage, setModelMarkers: setModelMarkers, getModelMarkers: getModelMarkers, + onDidChangeMarkers: onDidChangeMarkers, getModels: getModels, getModel: getModel, onDidCreateModel: onDidCreateModel, @@ -353,6 +363,7 @@ export function createMonacoEditorAPI(): typeof monaco.editor { defineTheme: defineTheme, setTheme: setTheme, remeasureFonts: remeasureFonts, + registerCommand: registerCommand, // enums AccessibilitySupport: standaloneEnums.AccessibilitySupport, diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index dbf225a6c..b75a81443 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; +import { Color } from 'vs/base/common/color'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -87,14 +88,14 @@ export class EncodedTokenizationSupport2Adapter implements modes.ITokenizationSu return this._actual.getInitialState(); } - public tokenize(line: string, state: modes.IState, offsetDelta: number): TokenizationResult { + public tokenize(line: string, hasEOL: boolean, state: modes.IState, offsetDelta: number): TokenizationResult { if (typeof this._actual.tokenize === 'function') { return TokenizationSupport2Adapter.adaptTokenize(this._languageIdentifier.language, <{ tokenize(line: string, state: modes.IState): ILineTokens; }>this._actual, line, state, offsetDelta); } throw new Error('Not supported!'); } - public tokenize2(line: string, state: modes.IState): TokenizationResult2 { + public tokenize2(line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 { let result = this._actual.tokenizeEncoded(line, state); return new TokenizationResult2(result.tokens, result.endState); } @@ -157,7 +158,7 @@ export class TokenizationSupport2Adapter implements modes.ITokenizationSupport { return new TokenizationResult(tokens, endState); } - public tokenize(line: string, state: modes.IState, offsetDelta: number): TokenizationResult { + public tokenize(line: string, hasEOL: boolean, state: modes.IState, offsetDelta: number): TokenizationResult { return TokenizationSupport2Adapter.adaptTokenize(this._languageIdentifier.language, this._actual, line, state, offsetDelta); } @@ -199,7 +200,7 @@ export class TokenizationSupport2Adapter implements modes.ITokenizationSupport { return actualResult; } - public tokenize2(line: string, state: modes.IState, offsetDelta: number): TokenizationResult2 { + public tokenize2(line: string, hasEOL: boolean, state: modes.IState, offsetDelta: number): TokenizationResult2 { let actualResult = this._actual.tokenize(line, state); let tokens = this._toBinaryTokens(actualResult.tokens, offsetDelta); @@ -310,6 +311,22 @@ function isThenable(obj: any): obj is Thenable { return obj && typeof obj.then === 'function'; } +/** + * Change the color map that is used for token colors. + * Supported formats (hex): #RRGGBB, $RRGGBBAA, #RGB, #RGBA + */ +export function setColorMap(colorMap: string[] | null): void { + if (colorMap) { + const result: Color[] = [null!]; + for (let i = 1, len = colorMap.length; i < len; i++) { + result[i] = Color.fromHex(colorMap[i]); + } + StaticServices.standaloneThemeService.get().setColorMapOverride(result); + } else { + StaticServices.standaloneThemeService.get().setColorMapOverride(null); + } +} + /** * Set the tokens provider for a language (manual implementation). */ @@ -570,6 +587,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { // provider methods setLanguageConfiguration: setLanguageConfiguration, + setColorMap: setColorMap, setTokensProvider: setTokensProvider, setMonarchTokensProvider: setMonarchTokensProvider, registerReferenceProvider: registerReferenceProvider, diff --git a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts index 544d7b07a..f61c9de85 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts @@ -39,7 +39,11 @@ class StandaloneTheme implements IStandaloneTheme { this.themeData = standaloneThemeData; let base = standaloneThemeData.base; if (name.length > 0) { - this.id = base + ' ' + name; + if (isBuiltinTheme(name)) { + this.id = name; + } else { + this.id = base + ' ' + name; + } this.themeName = name; } else { this.id = base; @@ -199,6 +203,7 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon private _allCSS: string; private _globalStyleElement: HTMLStyleElement | null; private _styleElements: HTMLStyleElement[]; + private _colorMapOverride: Color[] | null; private _theme!: IStandaloneTheme; constructor() { @@ -216,6 +221,7 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`; this._globalStyleElement = null; this._styleElements = []; + this._colorMapOverride = null; this.setTheme(VS_THEME_NAME); iconRegistry.onDidChange(() => { @@ -284,6 +290,11 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon return this._theme; } + public setColorMapOverride(colorMapOverride: Color[] | null): void { + this._colorMapOverride = colorMapOverride; + this._updateThemeOrColorMap(); + } + public setTheme(themeName: string): string { let theme: StandaloneTheme; if (this._knownThemes.has(themeName)) { @@ -296,7 +307,11 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon return theme.id; } this._theme = theme; + this._updateThemeOrColorMap(); + return theme.id; + } + private _updateThemeOrColorMap(): void { let cssRules: string[] = []; let hasRule: { [rule: string]: boolean; } = {}; let ruleCollector: ICssStyleCollector = { @@ -307,19 +322,16 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon } } }; - themingRegistry.getThemingParticipants().forEach(p => p(theme, ruleCollector, this._environment)); + themingRegistry.getThemingParticipants().forEach(p => p(this._theme, ruleCollector, this._environment)); - let tokenTheme = theme.tokenTheme; - let colorMap = tokenTheme.getColorMap(); + const colorMap = this._colorMapOverride || this._theme.tokenTheme.getColorMap(); ruleCollector.addRule(generateTokensCSSForColorMap(colorMap)); this._themeCSS = cssRules.join('\n'); this._updateCSS(); TokenizationRegistry.setColorMap(colorMap); - this._onColorThemeChange.fire(theme); - - return theme.id; + this._onColorThemeChange.fire(this._theme); } private _updateCSS(): void { diff --git a/src/vs/editor/standalone/common/monarch/monarchCommon.ts b/src/vs/editor/standalone/common/monarch/monarchCommon.ts index 6dbf14dc8..bd96a9f08 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCommon.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCommon.ts @@ -22,6 +22,7 @@ export const enum MonarchBracket { export interface ILexerMin { languageId: string; + includeLF: boolean; noThrow: boolean; ignoreCase: boolean; unicode: boolean; diff --git a/src/vs/editor/standalone/common/monarch/monarchCompile.ts b/src/vs/editor/standalone/common/monarch/monarchCompile.ts index 289d045ab..aa487e7e6 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCompile.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCompile.ts @@ -395,6 +395,7 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm // Create our lexer let lexer: monarchCommon.ILexer = {}; lexer.languageId = languageId; + lexer.includeLF = bool(json.includeLF, false); lexer.noThrow = false; // raise exceptions during compilation lexer.maxStack = 100; @@ -411,6 +412,7 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm // For calling compileAction later on let lexerMin: monarchCommon.ILexerMin = json; lexerMin.languageId = languageId; + lexerMin.includeLF = lexer.includeLF; lexerMin.ignoreCase = lexer.ignoreCase; lexerMin.unicode = lexer.unicode; lexerMin.noThrow = lexer.noThrow; diff --git a/src/vs/editor/standalone/common/monarch/monarchLexer.ts b/src/vs/editor/standalone/common/monarch/monarchLexer.ts index e566f7589..dc8b86443 100644 --- a/src/vs/editor/standalone/common/monarch/monarchLexer.ts +++ b/src/vs/editor/standalone/common/monarch/monarchLexer.ts @@ -231,7 +231,7 @@ class MonarchLineState implements modes.IState { interface IMonarchTokensCollector { enterMode(startOffset: number, modeId: string): void; emit(startOffset: number, type: string): void; - nestedModeTokenize(embeddedModeLine: string, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState; + nestedModeTokenize(embeddedModeLine: string, hasEOL: boolean, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState; } class MonarchClassicTokensCollector implements IMonarchTokensCollector { @@ -261,7 +261,7 @@ class MonarchClassicTokensCollector implements IMonarchTokensCollector { this._tokens.push(new Token(startOffset, type, this._language!)); } - public nestedModeTokenize(embeddedModeLine: string, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState { + public nestedModeTokenize(embeddedModeLine: string, hasEOL: boolean, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState { const nestedModeId = embeddedModeData.modeId; const embeddedModeState = embeddedModeData.state; @@ -272,7 +272,7 @@ class MonarchClassicTokensCollector implements IMonarchTokensCollector { return embeddedModeState; } - let nestedResult = nestedModeTokenizationSupport.tokenize(embeddedModeLine, embeddedModeState, offsetDelta); + let nestedResult = nestedModeTokenizationSupport.tokenize(embeddedModeLine, hasEOL, embeddedModeState, offsetDelta); this._tokens = this._tokens.concat(nestedResult.tokens); this._lastTokenType = null; this._lastTokenLanguage = null; @@ -345,7 +345,7 @@ class MonarchModernTokensCollector implements IMonarchTokensCollector { return result; } - public nestedModeTokenize(embeddedModeLine: string, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState { + public nestedModeTokenize(embeddedModeLine: string, hasEOL: boolean, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState { const nestedModeId = embeddedModeData.modeId; const embeddedModeState = embeddedModeData.state; @@ -356,7 +356,7 @@ class MonarchModernTokensCollector implements IMonarchTokensCollector { return embeddedModeState; } - let nestedResult = nestedModeTokenizationSupport.tokenize2(embeddedModeLine, embeddedModeState, offsetDelta); + let nestedResult = nestedModeTokenizationSupport.tokenize2(embeddedModeLine, hasEOL, embeddedModeState, offsetDelta); this._prependTokens = MonarchModernTokensCollector._merge(this._prependTokens, this._tokens, nestedResult.tokens); this._tokens = []; this._currentLanguageId = 0; @@ -456,23 +456,23 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { return MonarchLineStateFactory.create(rootState, null); } - public tokenize(line: string, lineState: modes.IState, offsetDelta: number): TokenizationResult { + public tokenize(line: string, hasEOL: boolean, lineState: modes.IState, offsetDelta: number): TokenizationResult { let tokensCollector = new MonarchClassicTokensCollector(); - let endLineState = this._tokenize(line, lineState, offsetDelta, tokensCollector); + let endLineState = this._tokenize(line, hasEOL, lineState, offsetDelta, tokensCollector); return tokensCollector.finalize(endLineState); } - public tokenize2(line: string, lineState: modes.IState, offsetDelta: number): TokenizationResult2 { + public tokenize2(line: string, hasEOL: boolean, lineState: modes.IState, offsetDelta: number): TokenizationResult2 { let tokensCollector = new MonarchModernTokensCollector(this._modeService, this._standaloneThemeService.getColorTheme().tokenTheme); - let endLineState = this._tokenize(line, lineState, offsetDelta, tokensCollector); + let endLineState = this._tokenize(line, hasEOL, lineState, offsetDelta, tokensCollector); return tokensCollector.finalize(endLineState); } - private _tokenize(line: string, lineState: MonarchLineState, offsetDelta: number, collector: IMonarchTokensCollector): MonarchLineState { + private _tokenize(line: string, hasEOL: boolean, lineState: MonarchLineState, offsetDelta: number, collector: IMonarchTokensCollector): MonarchLineState { if (lineState.embeddedModeData) { - return this._nestedTokenize(line, lineState, offsetDelta, collector); + return this._nestedTokenize(line, hasEOL, lineState, offsetDelta, collector); } else { - return this._myTokenize(line, lineState, offsetDelta, collector); + return this._myTokenize(line, hasEOL, lineState, offsetDelta, collector); } } @@ -518,24 +518,24 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { return popOffset; } - private _nestedTokenize(line: string, lineState: MonarchLineState, offsetDelta: number, tokensCollector: IMonarchTokensCollector): MonarchLineState { + private _nestedTokenize(line: string, hasEOL: boolean, lineState: MonarchLineState, offsetDelta: number, tokensCollector: IMonarchTokensCollector): MonarchLineState { let popOffset = this._findLeavingNestedModeOffset(line, lineState); if (popOffset === -1) { // tokenization will not leave nested mode - let nestedEndState = tokensCollector.nestedModeTokenize(line, lineState.embeddedModeData!, offsetDelta); + let nestedEndState = tokensCollector.nestedModeTokenize(line, hasEOL, lineState.embeddedModeData!, offsetDelta); return MonarchLineStateFactory.create(lineState.stack, new EmbeddedModeData(lineState.embeddedModeData!.modeId, nestedEndState)); } let nestedModeLine = line.substring(0, popOffset); if (nestedModeLine.length > 0) { // tokenize with the nested mode - tokensCollector.nestedModeTokenize(nestedModeLine, lineState.embeddedModeData!, offsetDelta); + tokensCollector.nestedModeTokenize(nestedModeLine, false, lineState.embeddedModeData!, offsetDelta); } let restOfTheLine = line.substring(popOffset); - return this._myTokenize(restOfTheLine, lineState, offsetDelta + popOffset, tokensCollector); + return this._myTokenize(restOfTheLine, hasEOL, lineState, offsetDelta + popOffset, tokensCollector); } private _safeRuleName(rule: monarchCommon.IRule | null): string { @@ -545,9 +545,11 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { return '(unknown)'; } - private _myTokenize(line: string, lineState: MonarchLineState, offsetDelta: number, tokensCollector: IMonarchTokensCollector): MonarchLineState { + private _myTokenize(lineWithoutLF: string, hasEOL: boolean, lineState: MonarchLineState, offsetDelta: number, tokensCollector: IMonarchTokensCollector): MonarchLineState { tokensCollector.enterMode(offsetDelta, this._modeId); + const lineWithoutLFLength = lineWithoutLF.length; + const line = (hasEOL && this._lexer.includeLF ? lineWithoutLF + '\n' : lineWithoutLF); const lineLength = line.length; let embeddedModeData = lineState.embeddedModeData; @@ -563,7 +565,7 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { } let groupMatching: GroupMatching | null = null; - // See https://github.com/microsoft/monaco-editor/issues/1235: + // See https://github.com/microsoft/monaco-editor/issues/1235 // Evaluate rules at least once for an empty line let forceEvaluation = true; @@ -752,8 +754,8 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { if (pos < lineLength) { // there is content from the embedded mode on this line - const restOfLine = line.substr(pos); - return this._nestedTokenize(restOfLine, MonarchLineStateFactory.create(stack, embeddedModeData), offsetDelta + pos, tokensCollector); + const restOfLine = lineWithoutLF.substr(pos); + return this._nestedTokenize(restOfLine, hasEOL, MonarchLineStateFactory.create(stack, embeddedModeData), offsetDelta + pos, tokensCollector); } else { return MonarchLineStateFactory.create(stack, embeddedModeData); } @@ -831,7 +833,9 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { tokenType = monarchCommon.sanitize(token); } - tokensCollector.emit(pos0 + offsetDelta, tokenType); + if (pos0 < lineWithoutLFLength) { + tokensCollector.emit(pos0 + offsetDelta, tokenType); + } } if (enteringEmbeddedMode !== null) { diff --git a/src/vs/editor/standalone/common/monarch/monarchTypes.ts b/src/vs/editor/standalone/common/monarch/monarchTypes.ts index 19936be8d..5e3a798c6 100644 --- a/src/vs/editor/standalone/common/monarch/monarchTypes.ts +++ b/src/vs/editor/standalone/common/monarch/monarchTypes.ts @@ -41,6 +41,11 @@ export interface IMonarchLanguage { * attach this to every token class (by default '.' + name) */ tokenPostfix?: string; + /** + * include line feeds (in the form of a \n character) at the end of lines + * Defaults to false + */ + includeLF?: boolean; } /** diff --git a/src/vs/editor/standalone/common/standaloneThemeService.ts b/src/vs/editor/standalone/common/standaloneThemeService.ts index c70a713dd..afefd369d 100644 --- a/src/vs/editor/standalone/common/standaloneThemeService.ts +++ b/src/vs/editor/standalone/common/standaloneThemeService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Color } from 'vs/base/common/color'; import { ITokenThemeRule, TokenTheme } from 'vs/editor/common/modes/supports/tokenization'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; @@ -33,4 +34,7 @@ export interface IStandaloneThemeService extends IThemeService { defineTheme(themeName: string, themeData: IStandaloneThemeData): void; getColorTheme(): IStandaloneTheme; + + setColorMapOverride(colorMapOverride: Color[] | null): void; + } diff --git a/src/vs/editor/standalone/common/themes.ts b/src/vs/editor/standalone/common/themes.ts index 4c7761e66..9e07df46b 100644 --- a/src/vs/editor/standalone/common/themes.ts +++ b/src/vs/editor/standalone/common/themes.ts @@ -65,7 +65,7 @@ export const vs: IStandaloneThemeData = { { token: 'operator.scss', foreground: '666666' }, { token: 'operator.sql', foreground: '778899' }, { token: 'operator.swift', foreground: '666666' }, - { token: 'predefined.sql', foreground: 'FF00FF' }, + { token: 'predefined.sql', foreground: 'C700C7' }, ], colors: { [editorBackground]: '#FFFFFE', diff --git a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts index e3c7a31f1..bd4399d6c 100644 --- a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts @@ -68,7 +68,8 @@ suite('TokenizationSupport2Adapter', () => { tokenColorMap: [] }; } - + setColorMapOverride(colorMapOverride: Color[] | null): void { + } public getFileIconTheme(): IFileIconTheme { return { hasFileIcons: false, @@ -107,15 +108,15 @@ suite('TokenizationSupport2Adapter', () => { const adapter = new TokenizationSupport2Adapter(new MockThemeService(), languageIdentifier, new BadTokensProvider()); - const actualClassicTokens = adapter.tokenize('whatever', MockState.INSTANCE, offsetDelta); - assert.deepEqual(actualClassicTokens.tokens, expectedClassicTokens); + const actualClassicTokens = adapter.tokenize('whatever', true, MockState.INSTANCE, offsetDelta); + assert.deepStrictEqual(actualClassicTokens.tokens, expectedClassicTokens); - const actualModernTokens = adapter.tokenize2('whatever', MockState.INSTANCE, offsetDelta); + const actualModernTokens = adapter.tokenize2('whatever', true, MockState.INSTANCE, offsetDelta); const modernTokens: number[] = []; for (let i = 0; i < actualModernTokens.tokens.length; i++) { modernTokens[i] = actualModernTokens.tokens[i]; } - assert.deepEqual(modernTokens, expectedModernTokens); + assert.deepStrictEqual(modernTokens, expectedModernTokens); } test('tokens always start at index 0 (no offset delta)', () => { diff --git a/src/vs/editor/standalone/test/monarch/monarch.test.ts b/src/vs/editor/standalone/test/monarch/monarch.test.ts index 89eb03cef..61fcafe55 100644 --- a/src/vs/editor/standalone/test/monarch/monarch.test.ts +++ b/src/vs/editor/standalone/test/monarch/monarch.test.ts @@ -68,39 +68,154 @@ suite('Monarch', () => { const actualTokens: Token[][] = []; let state = tokenizer.getInitialState(); for (const line of lines) { - const result = tokenizer.tokenize(line, state, 0); + const result = tokenizer.tokenize(line, true, state, 0); actualTokens.push(result.tokens); state = result.endState; } - assert.deepEqual(actualTokens, [ + assert.deepStrictEqual(actualTokens, [ [ - { 'offset': 0, 'type': 'source.test1', 'language': 'test1' }, - { 'offset': 12, 'type': 'string.quote.test1', 'language': 'test1' }, - { 'offset': 15, 'type': 'token.sql', 'language': 'sql' }, - { 'offset': 61, 'type': 'string.quote.test1', 'language': 'test1' }, - { 'offset': 64, 'type': 'source.test1', 'language': 'test1' } + new Token(0, 'source.test1', 'test1'), + new Token(12, 'string.quote.test1', 'test1'), + new Token(15, 'token.sql', 'sql'), + new Token(61, 'string.quote.test1', 'test1'), + new Token(64, 'source.test1', 'test1') ], [ - { 'offset': 0, 'type': 'source.test1', 'language': 'test1' }, - { 'offset': 12, 'type': 'string.quote.test1', 'language': 'test1' } + new Token(0, 'source.test1', 'test1'), + new Token(12, 'string.quote.test1', 'test1') ], [ - { 'offset': 0, 'type': 'token.sql', 'language': 'sql' } + new Token(0, 'token.sql', 'sql') ], [ - { 'offset': 0, 'type': 'token.sql', 'language': 'sql' } + new Token(0, 'token.sql', 'sql') ], [ - { 'offset': 0, 'type': 'token.sql', 'language': 'sql' } + new Token(0, 'token.sql', 'sql') ], [ - { 'offset': 0, 'type': 'string.quote.test1', 'language': 'test1' }, - { 'offset': 3, 'type': 'source.test1', 'language': 'test1' } + new Token(0, 'string.quote.test1', 'test1'), + new Token(3, 'source.test1', 'test1') ] ]); innerModeTokenizationRegistration.dispose(); innerModeRegistration.dispose(); }); + test('microsoft/monaco-editor#1235: Empty Line Handling', () => { + const modeService = new ModeServiceImpl(); + const tokenizer = createMonarchTokenizer(modeService, 'test', { + tokenizer: { + root: [ + { include: '@comments' }, + ], + + comments: [ + [/\/\/$/, 'comment'], // empty single-line comment + [/\/\//, 'comment', '@comment_cpp'], + ], + + comment_cpp: [ + [/(?:[^\\]|(?:\\.))+$/, 'comment', '@pop'], + [/.+$/, 'comment'], + [/$/, 'comment', '@pop'] + // No possible rule to detect an empty line and @pop? + ], + }, + }); + + const lines = [ + `// This comment \\`, + ` continues on the following line`, + ``, + `// This comment does NOT continue \\\\`, + ` because the escape char was itself escaped`, + ``, + `// This comment DOES continue because \\\\\\`, + ` the 1st '\\' escapes the 2nd; the 3rd escapes EOL`, + ``, + `// This comment continues to the following line \\`, + ``, + `But the line was empty. This line should not be commented.`, + ]; + + const actualTokens: Token[][] = []; + let state = tokenizer.getInitialState(); + for (const line of lines) { + const result = tokenizer.tokenize(line, true, state, 0); + actualTokens.push(result.tokens); + state = result.endState; + } + + assert.deepStrictEqual(actualTokens, [ + [new Token(0, 'comment.test', 'test')], + [new Token(0, 'comment.test', 'test')], + [], + [new Token(0, 'comment.test', 'test')], + [new Token(0, 'source.test', 'test')], + [], + [new Token(0, 'comment.test', 'test')], + [new Token(0, 'comment.test', 'test')], + [], + [new Token(0, 'comment.test', 'test')], + [], + [new Token(0, 'source.test', 'test')] + ]); + + }); + + test('microsoft/monaco-editor#2265: Exit a state at end of line', () => { + const modeService = new ModeServiceImpl(); + const tokenizer = createMonarchTokenizer(modeService, 'test', { + includeLF: true, + tokenizer: { + root: [ + [/^\*/, '', '@inner'], + [/\:\*/, '', '@inner'], + [/[^*:]+/, 'string'], + [/[*:]/, 'string'] + ], + inner: [ + [/\n/, '', '@pop'], + [/\d+/, 'number'], + [/[^\d]+/, ''] + ] + } + }); + + const lines = [ + `PRINT 10 * 20`, + `*FX200, 3`, + `PRINT 2*3:*FX200, 3` + ]; + + const actualTokens: Token[][] = []; + let state = tokenizer.getInitialState(); + for (const line of lines) { + const result = tokenizer.tokenize(line, true, state, 0); + actualTokens.push(result.tokens); + state = result.endState; + } + + assert.deepStrictEqual(actualTokens, [ + [ + new Token(0, 'string.test', 'test'), + ], + [ + new Token(0, '', 'test'), + new Token(3, 'number.test', 'test'), + new Token(6, '', 'test'), + new Token(8, 'number.test', 'test'), + ], + [ + new Token(0, 'string.test', 'test'), + new Token(9, '', 'test'), + new Token(13, 'number.test', 'test'), + new Token(16, '', 'test'), + new Token(18, 'number.test', 'test'), + ] + ]); + }); + }); diff --git a/src/vs/editor/test/browser/commands/shiftCommand.test.ts b/src/vs/editor/test/browser/commands/shiftCommand.test.ts index 755aac5bc..c31f8e4bf 100644 --- a/src/vs/editor/test/browser/commands/shiftCommand.test.ts +++ b/src/vs/editor/test/browser/commands/shiftCommand.test.ts @@ -964,7 +964,7 @@ suite('Editor Commands - ShiftCommand', () => { autoIndent: EditorAutoIndentStrategy.Full, }); let actual = getEditOperation(model, op); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } @@ -979,7 +979,7 @@ suite('Editor Commands - ShiftCommand', () => { autoIndent: EditorAutoIndentStrategy.Full, }); let actual = getEditOperation(model, op); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } }); diff --git a/src/vs/editor/test/browser/commands/sideEditing.test.ts b/src/vs/editor/test/browser/commands/sideEditing.test.ts index be07a5682..f8a8bbae5 100644 --- a/src/vs/editor/test/browser/commands/sideEditing.test.ts +++ b/src/vs/editor/test/browser/commands/sideEditing.test.ts @@ -19,10 +19,10 @@ function testCommand(lines: string[], selections: Selection[], edits: IIdentifie model.applyEdits(edits); - assert.deepEqual(model.getLinesContent(), expectedLines); + assert.deepStrictEqual(model.getLinesContent(), expectedLines); let actualSelections = viewModel.getSelections(); - assert.deepEqual(actualSelections.map(s => s.toString()), expectedSelections.map(s => s.toString())); + assert.deepStrictEqual(actualSelections.map(s => s.toString()), expectedSelections.map(s => s.toString())); }); } @@ -202,7 +202,7 @@ suite('SideEditing', () => { forceMoveMarkers: editForceMoveMarkers }]); const actual = viewModel.getSelection(); - assert.deepEqual(actual.toString(), expected.toString(), msg); + assert.deepStrictEqual(actual.toString(), expected.toString(), msg); }); } diff --git a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts index af0c64e53..d9fdfc1bd 100644 --- a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts +++ b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts @@ -37,14 +37,14 @@ function assertTrimTrailingWhitespaceCommand(text: string[], expected: IIdentifi return withEditorModel(text, (model) => { let op = new TrimTrailingWhitespaceCommand(new Selection(1, 1, 1, 1), []); let actual = getEditOperation(model, op); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } function assertTrimTrailingWhitespace(text: string[], cursors: Position[], expected: IIdentifiedSingleEditOperation[]): void { return withEditorModel(text, (model) => { let actual = trimTrailingWhitespace(model, cursors); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index cb202a3f8..489af4718 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -115,7 +115,7 @@ function assertCursor(viewModel: ViewModel, what: Position | Selection | Selecti let actual = viewModel.getSelections().map(s => s.toString()); let expected = selections.map(s => s.toString()); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } suite('Editor Controller - Cursor', () => { @@ -795,11 +795,11 @@ suite('Editor Controller - Cursor', () => { viewModel.onEvent((e) => { if (e.kind === OutgoingViewModelEventKind.CursorStateChanged) { events++; - assert.deepEqual(e.selections, [new Selection(1, 2, 1, 2)]); + assert.deepStrictEqual(e.selections, [new Selection(1, 2, 1, 2)]); } }); moveTo(editor, viewModel, 1, 2); - assert.equal(events, 1, 'receives 1 event'); + assert.strictEqual(events, 1, 'receives 1 event'); }); }); @@ -809,11 +809,11 @@ suite('Editor Controller - Cursor', () => { viewModel.onEvent((e) => { if (e.kind === OutgoingViewModelEventKind.CursorStateChanged) { events++; - assert.deepEqual(e.selections, [new Selection(1, 1, 1, 2)]); + assert.deepStrictEqual(e.selections, [new Selection(1, 1, 1, 2)]); } }); moveTo(editor, viewModel, 1, 2, true); - assert.equal(events, 1, 'receives 1 event'); + assert.strictEqual(events, 1, 'receives 1 event'); }); }); @@ -1311,7 +1311,7 @@ suite('Editor Controller - Regression tests', () => { // Check that indenting maintains the selection start at column 1 CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.deepEqual(viewModel.getSelection(), new Selection(1, 1, 1, 14)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 1, 1, 14)); }); model.dispose(); @@ -1330,49 +1330,49 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\t', 'assert3'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\n\t', 'assert3'); viewModel.type('x'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert4'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert4'); CoreNavigationCommands.CursorLeft.runCoreEditorCommand(viewModel, {}); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert5'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert5'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert6'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert6'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\tx', 'assert7'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\tx', 'assert7'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert8'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert8'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert9'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x', 'assert9'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert10'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert10'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert11'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert11'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert12'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert12'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert13'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert13'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert14'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert14'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert15'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x', 'assert15'); }); model.dispose(); @@ -1387,13 +1387,13 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Position(1, 1)); model.setEOL(EndOfLineSequence.LF); - assert.equal(model.getValue(), 'Hello\nworld'); + assert.strictEqual(model.getValue(), 'Hello\nworld'); model.pushEOL(EndOfLineSequence.CRLF); - assert.equal(model.getValue(), 'Hello\r\nworld'); + assert.strictEqual(model.getValue(), 'Hello\r\nworld'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), 'Hello\nworld'); + assert.strictEqual(model.getValue(), 'Hello\nworld'); }); }); @@ -1415,10 +1415,10 @@ suite('Editor Controller - Regression tests', () => { editor.setSelection(new Selection(1, 1, 1, 2)); viewModel.type('%', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '%\'%👁\'', 'assert1'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '%\'%👁\'', 'assert1'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\'👁\'', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\'👁\'', 'assert2'); }); model.dispose(); @@ -1433,50 +1433,50 @@ suite('Editor Controller - Regression tests', () => { viewModel.type(' ', 'keyboard'); viewModel.type('world', 'keyboard'); viewModel.type(' ', 'keyboard'); - assert.equal(model.getLineContent(1), 'Hello world '); + assert.strictEqual(model.getLineContent(1), 'Hello world '); assertCursor(viewModel, new Position(1, 13)); moveLeft(editor, viewModel); moveRight(editor, viewModel); model.pushEditOperations([], [EditOperation.replaceMove(new Range(1, 12, 1, 13), '')], () => []); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world '); + assert.strictEqual(model.getLineContent(1), 'Hello world '); assertCursor(viewModel, new Selection(1, 12, 1, 13)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello'); + assert.strictEqual(model.getLineContent(1), 'Hello'); assertCursor(viewModel, new Position(1, 6)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(1), ''); assertCursor(viewModel, new Position(1, 1)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello'); + assert.strictEqual(model.getLineContent(1), 'Hello'); assertCursor(viewModel, new Position(1, 6)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world '); + assert.strictEqual(model.getLineContent(1), 'Hello world '); assertCursor(viewModel, new Position(1, 13)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); }); @@ -1498,7 +1498,7 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(1, 6, 1, 6)); CoreEditingCommands.Outdent.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' function baz() {'); + assert.strictEqual(model.getLineContent(1), ' function baz() {'); assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); @@ -1518,7 +1518,7 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(1, 7, 1, 7)); CoreEditingCommands.Outdent.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(1), ' '); assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); @@ -1540,7 +1540,7 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(1, 9, 1, 9)); CoreEditingCommands.Outdent.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(1), ' '); assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); @@ -1568,7 +1568,7 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(7, 1, 7, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(7), '\t'); + assert.strictEqual(model.getLineContent(7), '\t'); assertCursor(viewModel, new Selection(7, 2, 7, 2)); }); @@ -1588,8 +1588,8 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(2, 1, 2, 1)); viewModel.cut('keyboard'); - assert.equal(model.getLineCount(), 1); - assert.equal(model.getLineContent(1), 'asdasd'); + assert.strictEqual(model.getLineCount(), 1); + assert.strictEqual(model.getLineContent(1), 'asdasd'); }); @@ -1604,12 +1604,12 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(2, 1, 2, 1)); viewModel.cut('keyboard'); - assert.equal(model.getLineCount(), 1); - assert.equal(model.getLineContent(1), 'asdasd'); + assert.strictEqual(model.getLineCount(), 1); + assert.strictEqual(model.getLineContent(1), 'asdasd'); viewModel.cut('keyboard'); - assert.equal(model.getLineCount(), 1); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineCount(), 1); + assert.strictEqual(model.getLineContent(1), ''); }); }); @@ -1651,8 +1651,8 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assertCursor(viewModel, new Selection(1, 14, 1, 14)); - assert.equal(model.getLineCount(), 1); - assert.equal(model.getLineContent(1), 'function baz(;'); + assert.strictEqual(model.getLineCount(), 1); + assert.strictEqual(model.getLineContent(1), 'function baz(;'); }); model.dispose(); @@ -1671,9 +1671,9 @@ suite('Editor Controller - Regression tests', () => { viewModel.paste('line1\n', true); - assert.equal(model.getLineContent(1), 'line1'); - assert.equal(model.getLineContent(2), 'line1'); - assert.equal(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(1), 'line1'); + assert.strictEqual(model.getLineContent(2), 'line1'); + assert.strictEqual(model.getLineContent(3), ''); }); }); @@ -1689,10 +1689,10 @@ suite('Editor Controller - Regression tests', () => { viewModel.paste('line1\n', true); - assert.equal(model.getLineContent(1), 'line1'); - assert.equal(model.getLineContent(2), 'line line1'); - assert.equal(model.getLineContent(3), ' 2'); - assert.equal(model.getLineContent(4), 'line3'); + assert.strictEqual(model.getLineContent(1), 'line1'); + assert.strictEqual(model.getLineContent(2), 'line line1'); + assert.strictEqual(model.getLineContent(3), ' 2'); + assert.strictEqual(model.getLineContent(4), 'line3'); }); }); @@ -1715,7 +1715,7 @@ suite('Editor Controller - Regression tests', () => { ] ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'a', 'bline1', 'c', @@ -1747,7 +1747,7 @@ suite('Editor Controller - Regression tests', () => { null ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'aaa', 'bbb', 'ccc', @@ -1790,7 +1790,7 @@ suite('Editor Controller - Regression tests', () => { null ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'aaa', 'bbb', 'ccc', @@ -1815,7 +1815,7 @@ suite('Editor Controller - Regression tests', () => { null ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'aline1', 'bline2', 'cline3' @@ -1839,7 +1839,7 @@ suite('Editor Controller - Regression tests', () => { null ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'aline1', 'bline2', 'cline3' @@ -1869,26 +1869,26 @@ suite('Editor Controller - Regression tests', () => { }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ '\t just some text' ].join('\n'), '001'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ ' some lines', ' and more lines', ' just some text', ].join('\n'), '002'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'some lines', 'and more lines', 'just some text', ].join('\n'), '003'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'some lines', 'and more lines', 'just some text', @@ -1911,7 +1911,7 @@ suite('Editor Controller - Regression tests', () => { viewModel.type('😍', 'keyboard'); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'some lines', 'and more lines', '😍just some text', @@ -1933,7 +1933,7 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { moveTo(editor, viewModel, 3, 2, false); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(3), '\t \tx: 3'); + assert.strictEqual(model.getLineContent(3), '\t \tx: 3'); }); model.dispose(); @@ -1954,7 +1954,7 @@ suite('Editor Controller - Regression tests', () => { moveTo(editor, viewModel, 1, 15, false); moveTo(editor, viewModel, 1, 22, true); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'var foo = 123;\t// this is a comment'); + assert.strictEqual(model.getLineContent(1), 'var foo = 123;\t// this is a comment'); }); model.dispose(); @@ -1982,8 +1982,8 @@ suite('Editor Controller - Regression tests', () => { CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(viewModel, args); } - assert.equal(viewModel.getSelection().startColumn, 1, 'TEST FOR ' + col); - assert.equal(viewModel.getSelection().endColumn, expectedCol, 'TEST FOR ' + col); + assert.strictEqual(viewModel.getSelection().startColumn, 1, 'TEST FOR ' + col); + assert.strictEqual(viewModel.getSelection().endColumn, expectedCol, 'TEST FOR ' + col); } assertWordRight(1, ' '.length + 1); @@ -2048,10 +2048,10 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { CoreNavigationCommands.WordSelect.runCoreEditorCommand(viewModel, { position: new Position(1, 8) }); - assert.deepEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(viewModel, { position: new Position(1, 8) }); - assert.deepEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); }); model.dispose(); @@ -2066,7 +2066,7 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { CoreNavigationCommands.WordSelect.runCoreEditorCommand(viewModel, { position: new Position(1, 5) }); - assert.deepEqual(viewModel.getSelection(), new Selection(1, 5, 1, 8)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 5, 1, 8)); }); model.dispose(); @@ -2090,11 +2090,11 @@ suite('Editor Controller - Regression tests', () => { viewModel.replacePreviousChar('せんせい', 4); viewModel.replacePreviousChar('せんせい', 4); - assert.equal(model.getLineContent(1), 'せんせい'); + assert.strictEqual(model.getLineContent(1), 'せんせい'); assertCursor(viewModel, new Position(1, 5)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(1), ''); assertCursor(viewModel, new Position(1, 1)); }); }); @@ -2121,11 +2121,11 @@ suite('Editor Controller - Regression tests', () => { viewModel.type('n', 'keyboard'); for (let i = 0; i < LINE_CNT; i++) { - assert.equal(model.getLineContent(i + 1), 'nnasd', 'line #' + (i + 1)); + assert.strictEqual(model.getLineContent(i + 1), 'nnasd', 'line #' + (i + 1)); } - assert.equal(viewModel.getSelections().length, LINE_CNT); - assert.equal(viewModel.getSelections()[LINE_CNT - 1].startLineNumber, LINE_CNT); + assert.strictEqual(viewModel.getSelections().length, LINE_CNT); + assert.strictEqual(viewModel.getSelections()[LINE_CNT - 1].startLineNumber, LINE_CNT); }); }); @@ -2349,6 +2349,37 @@ suite('Editor Controller - Regression tests', () => { }); }); + test('issue #112301: new stickyTabStops feature interferes with word wrap', () => { + withTestCodeEditor([ + [ + 'function hello() {', + ' console.log(`this is a long console message`)', + '}', + ].join('\n') + ], { wordWrap: 'wordWrapColumn', wordWrapColumn: 32, stickyTabStops: true }, (editor, viewModel) => { + viewModel.setSelections('test', [ + new Selection(2, 31, 2, 31) + ]); + moveRight(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 32)); + + moveRight(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 33)); + + moveRight(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 34)); + + moveLeft(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 33)); + + moveLeft(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 32)); + + moveLeft(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 31)); + }); + }); + test('issue #44805: Should not be able to undo in readonly editor', () => { let model = createTextModel( [ @@ -2361,10 +2392,10 @@ suite('Editor Controller - Regression tests', () => { range: new Range(1, 1, 1, 1), text: 'Hello world!' }], () => [new Selection(1, 1, 1, 1)]); - assert.equal(model.getValue(EndOfLinePreference.LF), 'Hello world!'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'Hello world!'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'Hello world!'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'Hello world!'); }); model.dispose(); @@ -2375,7 +2406,7 @@ suite('Editor Controller - Regression tests', () => { const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { return new TokenizationResult2(new Uint32Array(0), state); } }; @@ -2426,8 +2457,8 @@ suite('Editor Controller - Regression tests', () => { viewModel.type('\'', 'keyboard'); - assert.equal(model.getLineContent(1), 'const a = \'foo\';'); - assert.equal(model.getLineContent(2), 'const b = \'\''); + assert.strictEqual(model.getLineContent(1), 'const a = \'foo\';'); + assert.strictEqual(model.getLineContent(2), 'const b = \'\''); }); model.dispose(); @@ -2518,7 +2549,7 @@ suite('Editor Controller - Regression tests', () => { new Selection(2, 1, 2, 1) ]); viewModel.paste('something\n', true); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'abc123', 'something', '' @@ -2542,22 +2573,22 @@ suite('Editor Controller - Regression tests', () => { ]); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'สวัสด'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวัสด'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'สวัส'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวัส'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'สวั'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวั'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'สว'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สว'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'ส'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ส'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), ''); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), ''); }); model.dispose(); @@ -2578,8 +2609,8 @@ suite('Editor Controller - Cursor Configuration', () => { }, (editor, model, viewModel) => { CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(1, 21), source: 'keyboard' }); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' \tMy First Line\t '); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(1), ' \tMy First Line\t '); + assert.strictEqual(model.getLineContent(2), ' '); }); }); @@ -2602,56 +2633,56 @@ suite('Editor Controller - Cursor Configuration', () => { // Tab on column 1 CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 1) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' My Second Line123'); + assert.strictEqual(model.getLineContent(2), ' My Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 2 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 2) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'M y Second Line123'); + assert.strictEqual(model.getLineContent(2), 'M y Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 3 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 3) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 4 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 4) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 5 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 5) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My S econd Line123'); + assert.strictEqual(model.getLineContent(2), 'My S econd Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 5 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 5) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My S econd Line123'); + assert.strictEqual(model.getLineContent(2), 'My S econd Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 13 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 13) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My Second Li ne123'); + assert.strictEqual(model.getLineContent(2), 'My Second Li ne123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 14 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 14) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My Second Lin e123'); + assert.strictEqual(model.getLineContent(2), 'My Second Lin e123'); }); model.dispose(); @@ -2669,7 +2700,7 @@ suite('Editor Controller - Cursor Configuration', () => { assertCursor(viewModel, new Selection(1, 7, 1, 7)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); + assert.strictEqual(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); }); mode.dispose(); }); @@ -2686,7 +2717,7 @@ suite('Editor Controller - Cursor Configuration', () => { assertCursor(viewModel, new Selection(1, 7, 1, 7)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); + assert.strictEqual(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); }); mode.dispose(); }); @@ -2703,7 +2734,7 @@ suite('Editor Controller - Cursor Configuration', () => { assertCursor(viewModel, new Selection(1, 7, 1, 7)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.CRLF), '\thell(\r\n \r\n )'); + assert.strictEqual(model.getValue(EndOfLinePreference.CRLF), '\thell(\r\n \r\n )'); }); mode.dispose(); }); @@ -2721,14 +2752,14 @@ suite('Editor Controller - Cursor Configuration', () => { // Move cursor to the end, verify that we do not trim whitespaces if line has values moveTo(editor, viewModel, 1, model.getLineContent(1).length + 1); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ' '); // Try to enter again, we should trimmed previous line viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ' '); }); }); @@ -2740,16 +2771,47 @@ suite('Editor Controller - Cursor Configuration', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, model.getLineContent(1).length + 1); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' '); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(2), ' '); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' '); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ' '); }); }); + test('issue #115033: indent and appendText', () => { + const mode = new class extends MockMode { + constructor() { + super(new LanguageIdentifier('onEnterMode', 3)); + this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + onEnterRules: [{ + beforeText: /.*/, + action: { + indentAction: IndentAction.Indent, + appendText: 'x' + } + }] + })); + } + }(); + usingCursor({ + text: [ + 'text' + ], + languageIdentifier: mode.getLanguageIdentifier(), + }, (editor, model, viewModel) => { + + moveTo(editor, viewModel, 1, 5); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getLineContent(1), 'text'); + assert.strictEqual(model.getLineContent(2), ' x'); + assertCursor(viewModel, new Position(2, 6)); + }); + mode.dispose(); + }); + test('issue #6862: Editor removes auto inserted indentation when formatting on type', () => { let mode = new OnEnterMode(IndentAction.IndentOutdent); usingCursor({ @@ -2761,9 +2823,9 @@ suite('Editor Controller - Cursor Configuration', () => { moveTo(editor, viewModel, 1, 32); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), 'function foo (params: string) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), '}'); + assert.strictEqual(model.getLineContent(1), 'function foo (params: string) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), '}'); class TestCommand implements ICommand { @@ -2781,9 +2843,9 @@ suite('Editor Controller - Cursor Configuration', () => { } viewModel.executeCommand(new TestCommand(), 'autoFormat'); - assert.equal(model.getLineContent(1), 'function foo(params: string) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), '}'); + assert.strictEqual(model.getLineContent(1), 'function foo(params: string) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), '}'); }); mode.dispose(); }); @@ -2803,27 +2865,27 @@ suite('Editor Controller - Cursor Configuration', () => { moveTo(editor, viewModel, 3, 1); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' if (a) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ' '); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ' }'); + assert.strictEqual(model.getLineContent(1), ' if (a) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ' }'); moveTo(editor, viewModel, 4, 1); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' if (a) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), ' '); - assert.equal(model.getLineContent(5), ' }'); + assert.strictEqual(model.getLineContent(1), ' if (a) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), ' '); + assert.strictEqual(model.getLineContent(5), ' }'); moveTo(editor, viewModel, 5, model.getLineMaxColumn(5)); viewModel.type('something', 'keyboard'); - assert.equal(model.getLineContent(1), ' if (a) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ' }something'); + assert.strictEqual(model.getLineContent(1), ' if (a) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ' }something'); }); model.dispose(); @@ -2841,46 +2903,46 @@ suite('Editor Controller - Cursor Configuration', () => { // Move cursor to the end, verify that we do not trim whitespaces if line has values moveTo(editor, viewModel, 1, model.getLineContent(1).length + 1); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ' '); // Try to enter again, we should trimmed previous line viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ' '); // More whitespaces CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ' '); // Enter and verify that trimmed again viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), ' '); // Trimmed if we will keep only text moveTo(editor, viewModel, 1, 5); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' '); - assert.equal(model.getLineContent(2), ' some line abc '); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ''); + assert.strictEqual(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(2), ' some line abc '); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ''); // Trimmed if we will keep only text by selection moveTo(editor, viewModel, 2, 5); moveTo(editor, viewModel, 3, 1, true); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' '); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ' '); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ''); + assert.strictEqual(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ''); }); model.dispose(); @@ -2901,7 +2963,7 @@ suite('Editor Controller - Cursor Configuration', () => { moveTo(editor, viewModel, 3, model.getLineMaxColumn(3)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ ' function f() {', ' // I\'m gonna copy this line', ' return 3;', @@ -2911,7 +2973,7 @@ suite('Editor Controller - Cursor Configuration', () => { assertCursor(viewModel, new Position(4, model.getLineMaxColumn(4))); viewModel.paste(' // I\'m gonna copy this line\n', true); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ ' function f() {', ' // I\'m gonna copy this line', ' return 3;', @@ -2941,7 +3003,7 @@ suite('Editor Controller - Cursor Configuration', () => { editor.setSelections([new Selection(4, 10, 4, 10)]); viewModel.paste(' // I\'m gonna copy this line\n', true); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ ' function f() {', ' // I\'m gonna copy this line', ' // Another line', @@ -2968,7 +3030,7 @@ suite('Editor Controller - Cursor Configuration', () => { // DeleteLeft removes just one whitespace moveTo(editor, viewModel, 2, 9); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); }); model.dispose(); @@ -2987,54 +3049,54 @@ suite('Editor Controller - Cursor Configuration', () => { // DeleteLeft does not remove tab size, because some text exists before moveTo(editor, viewModel, 2, model.getLineContent(2).length + 1); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); // DeleteLeft removes tab size = 4 moveTo(editor, viewModel, 2, 9); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); // DeleteLeft removes tab size = 4 CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'a '); + assert.strictEqual(model.getLineContent(2), 'a '); // Undo DeleteLeft - get us back to original indentation CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); // Nothing is broken when cursor is in (1,1) moveTo(editor, viewModel, 1, 1); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' \t \t x'); + assert.strictEqual(model.getLineContent(1), ' \t \t x'); // DeleteLeft stops at tab stops even in mixed whitespace case moveTo(editor, viewModel, 1, 10); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' \t \t x'); + assert.strictEqual(model.getLineContent(1), ' \t \t x'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' \t \tx'); + assert.strictEqual(model.getLineContent(1), ' \t \tx'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' \tx'); + assert.strictEqual(model.getLineContent(1), ' \tx'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'x'); + assert.strictEqual(model.getLineContent(1), 'x'); // DeleteLeft on last line moveTo(editor, viewModel, 3, model.getLineContent(3).length + 1); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(3), ''); // DeleteLeft with removing new line symbol CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x\n a '); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x\n a '); // In case of selection DeleteLeft only deletes selected text moveTo(editor, viewModel, 2, 3); moveTo(editor, viewModel, 2, 4, true); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); }); model.dispose(); @@ -3052,55 +3114,55 @@ suite('Editor Controller - Cursor Configuration', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); viewModel.type('y', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty', 'assert2'); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\t', 'assert3'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\n\t', 'assert3'); viewModel.type('x'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert4'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert4'); CoreNavigationCommands.CursorLeft.runCoreEditorCommand(viewModel, {}); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert5'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert5'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert6'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert6'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\tyx', 'assert7'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\tyx', 'assert7'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\tx', 'assert8'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\tx', 'assert8'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert9'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert9'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert10'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x', 'assert10'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert11'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert11'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert12'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert12'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert13'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert13'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert14'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert14'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert15'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert15'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert16'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x', 'assert16'); }); model.dispose(); @@ -3124,8 +3186,8 @@ suite('Editor Controller - Cursor Configuration', () => { const afterVersion = model.getVersionId(); const afterAltVersion = model.getAlternativeVersionId(); - assert.notEqual(beforeVersion, afterVersion); - assert.equal(beforeAltVersion, afterAltVersion); + assert.notStrictEqual(beforeVersion, afterVersion); + assert.strictEqual(beforeAltVersion, afterAltVersion); }); model.dispose(); @@ -3181,7 +3243,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('}', 'keyboard'); assertCursor(viewModel, new Selection(2, 2, 2, 2)); - assert.equal(model.getLineContent(2), '}', '001'); + assert.strictEqual(model.getLineContent(2), '}', '001'); }); }); @@ -3274,7 +3336,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 1, 4, 1)); - assert.equal(model.getLineContent(3), 'return true;', '001'); + assert.strictEqual(model.getLineContent(3), 'return true;', '001'); }); }); @@ -3295,7 +3357,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(5, 1, 5, 1)); - assert.equal(model.getLineContent(4), '\t}', '001'); + assert.strictEqual(model.getLineContent(4), '\t}', '001'); }); }); @@ -3363,7 +3425,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(3, 16, 3, 16)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(3), ' if (true) {'); + assert.strictEqual(model.getLineContent(3), ' if (true) {'); assertCursor(viewModel, new Selection(4, 9, 4, 9)); }); }); @@ -3388,7 +3450,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(3, 16, 3, 16)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(3), ' if (true) {'); + assert.strictEqual(model.getLineContent(3), ' if (true) {'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); }); }); @@ -3411,7 +3473,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(5, 4, 5, 4)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(5), '\t\t}'); + assert.strictEqual(model.getLineContent(5), '\t\t}'); assertCursor(viewModel, new Selection(6, 3, 6, 3)); }); }); @@ -3432,7 +3494,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); - assert.equal(model.getLineContent(4), '\t\t true;', '001'); + assert.strictEqual(model.getLineContent(4), '\t\t true;', '001'); }); }); @@ -3452,7 +3514,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); - assert.equal(model.getLineContent(4), '\t\treturn true;', '001'); + assert.strictEqual(model.getLineContent(4), '\t\treturn true;', '001'); }); }); @@ -3471,7 +3533,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 5, 4, 5)); - assert.equal(model.getLineContent(4), ' true;', '001'); + assert.strictEqual(model.getLineContent(4), ' true;', '001'); }); }); @@ -3491,14 +3553,14 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 2, 4, 2)); - assert.equal(model.getLineContent(4), '\t\treturn true;', '001'); + assert.strictEqual(model.getLineContent(4), '\t\treturn true;', '001'); moveTo(editor, viewModel, 4, 1, false); assertCursor(viewModel, new Selection(4, 1, 4, 1)); viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(5, 1, 5, 1)); - assert.equal(model.getLineContent(5), '\t\treturn true;', '002'); + assert.strictEqual(model.getLineContent(5), '\t\treturn true;', '002'); }); }); @@ -3518,14 +3580,14 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); - assert.equal(model.getLineContent(4), '\t\t\treturn true;', '001'); + assert.strictEqual(model.getLineContent(4), '\t\t\treturn true;', '001'); moveTo(editor, viewModel, 4, 1, false); assertCursor(viewModel, new Selection(4, 1, 4, 1)); viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(5, 1, 5, 1)); - assert.equal(model.getLineContent(5), '\t\t\treturn true;', '002'); + assert.strictEqual(model.getLineContent(5), '\t\t\treturn true;', '002'); }); }); @@ -3544,12 +3606,12 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 2, 4, 2)); - assert.equal(model.getLineContent(4), ' return true;', '001'); + assert.strictEqual(model.getLineContent(4), ' return true;', '001'); moveTo(editor, viewModel, 4, 3, false); viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(5, 3, 5, 3)); - assert.equal(model.getLineContent(5), ' return true;', '002'); + assert.strictEqual(model.getLineContent(5), ' return true;', '002'); }); }); @@ -3577,12 +3639,12 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 4, 4, 4)); - assert.equal(model.getLineContent(4), ' return true;', '001'); + assert.strictEqual(model.getLineContent(4), ' return true;', '001'); moveTo(editor, viewModel, 9, 4, false); viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(10, 5, 10, 5)); - assert.equal(model.getLineContent(10), ' return true;', '001'); + assert.strictEqual(model.getLineContent(10), ' return true;', '001'); }); }); @@ -3604,7 +3666,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); - assert.equal(model.getLineContent(4), ' return true;', '001'); + assert.strictEqual(model.getLineContent(4), ' return true;', '001'); }); }); @@ -3626,7 +3688,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(3, 8, 2, 12)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(3), '\treturn x;'); + assert.strictEqual(model.getLineContent(3), '\treturn x;'); assertCursor(viewModel, new Position(3, 2)); }); }); @@ -3649,7 +3711,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(2, 12, 3, 8)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(3), '\treturn x;'); + assert.strictEqual(model.getLineContent(3), '\treturn x;'); assertCursor(viewModel, new Position(3, 2)); }); }); @@ -3670,9 +3732,9 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(5, 3, 5, 3)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(6), '\t'); + assert.strictEqual(model.getLineContent(6), '\t'); assertCursor(viewModel, new Selection(6, 2, 6, 2)); - assert.equal(model.getLineContent(5), '\t}'); + assert.strictEqual(model.getLineContent(5), '\t}'); }); }); @@ -3690,7 +3752,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 2, 4, 2)); - assert.equal(model.getLineContent(4), '\t'); + assert.strictEqual(model.getLineContent(4), '\t'); }); }); @@ -3715,7 +3777,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 1, 4, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t'); }); model.dispose(); @@ -3743,7 +3805,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 2, 4, 2)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t\t'); }); model.dispose(); @@ -3771,7 +3833,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 1, 4, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t\t'); }); model.dispose(); @@ -3798,7 +3860,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 3, 4, 3)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t\t\t'); }); model.dispose(); @@ -3825,7 +3887,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 4, 4, 4)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t\t\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t\t\t\t'); }); model.dispose(); @@ -3849,11 +3911,11 @@ suite('Editor Controller - Indentation Rules', () => { moveTo(editor, viewModel, 3, 1); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' if (a) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ' '); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ' }'); + assert.strictEqual(model.getLineContent(1), ' if (a) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ' }'); }); model.dispose(); @@ -3880,7 +3942,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 7, 4, 7)); viewModel.type('d', 'keyboard'); - assert.equal(model.getLineContent(4), ' end'); + assert.strictEqual(model.getLineContent(4), ' end'); }); rubyMode.dispose(); @@ -3903,7 +3965,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('e', 'keyboard'); assertCursor(viewModel, new Selection(5, 4, 5, 4)); - assert.equal(model.getLineContent(5), '\t}e', 'This line should not decrease indent'); + assert.strictEqual(model.getLineContent(5), '\t}e', 'This line should not decrease indent'); }); }); @@ -3924,7 +3986,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type(' ', 'keyboard'); assertCursor(viewModel, new Selection(2, 4, 2, 4)); - assert.equal(model.getLineContent(2), '\t ) {', 'This line should not decrease indent'); + assert.strictEqual(model.getLineContent(2), '\t ) {', 'This line should not decrease indent'); }); }); @@ -3943,7 +4005,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('}', 'keyboard'); assertCursor(viewModel, new Selection(3, 2, 3, 2)); - assert.equal(model.getLineContent(3), '}'); + assert.strictEqual(model.getLineContent(3), '}'); }); }); @@ -3990,7 +4052,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(7, 6, 7, 6)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(), + assert.strictEqual(model.getValue(), [ 'class ItemCtrl {', ' getPropertiesByItemId(id) {', @@ -4010,6 +4072,47 @@ suite('Editor Controller - Indentation Rules', () => { mode.dispose(); }); + test('issue #115304: OnEnter broken for TS', () => { + class JSMode extends MockMode { + private static readonly _id = new LanguageIdentifier('indentRulesMode', 4); + constructor() { + super(JSMode._id); + this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + onEnterRules: javascriptOnEnterRules + })); + } + } + + const mode = new JSMode(); + const model = createTextModel( + [ + '/** */', + 'function f() {}', + ].join('\n'), + undefined, + mode.getLanguageIdentifier() + ); + + withTestCodeEditor(null, { model: model, autoIndent: 'advanced' }, (editor, viewModel) => { + moveTo(editor, viewModel, 1, 4, false); + assertCursor(viewModel, new Selection(1, 4, 1, 4)); + + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), + [ + '/**', + ' * ', + ' */', + 'function f() {}', + ].join('\n') + ); + assertCursor(viewModel, new Selection(2, 4, 2, 4)); + }); + + model.dispose(); + mode.dispose(); + }); + test('issue #38261: TAB key results in bizarre indentation in C++ mode ', () => { class CppMode extends MockMode { private static readonly _id = new LanguageIdentifier('indentRulesMode', 4); @@ -4054,7 +4157,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(8, 1, 8, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), + assert.strictEqual(model.getValue(), [ 'int main() {', ' return 0;', @@ -4067,7 +4170,7 @@ suite('Editor Controller - Indentation Rules', () => { ')', ].join('\n') ); - assert.deepEqual(viewModel.getSelection(), new Selection(8, 3, 8, 3)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(8, 3, 8, 3)); }); model.dispose(); @@ -4144,31 +4247,43 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(3, 19, 3, 19)); viewModel.type('\n', 'keyboard'); - assert.deepEqual(model.getLineContent(4), ' '); + assert.deepStrictEqual(model.getLineContent(4), ' '); moveTo(editor, viewModel, 5, 18, false); assertCursor(viewModel, new Selection(5, 18, 5, 18)); viewModel.type('\n', 'keyboard'); - assert.deepEqual(model.getLineContent(6), ' '); + assert.deepStrictEqual(model.getLineContent(6), ' '); moveTo(editor, viewModel, 7, 15, false); assertCursor(viewModel, new Selection(7, 15, 7, 15)); viewModel.type('\n', 'keyboard'); - assert.deepEqual(model.getLineContent(8), ' '); - assert.deepEqual(model.getLineContent(9), ' ]'); + assert.deepStrictEqual(model.getLineContent(8), ' '); + assert.deepStrictEqual(model.getLineContent(9), ' ]'); moveTo(editor, viewModel, 10, 18, false); assertCursor(viewModel, new Selection(10, 18, 10, 18)); viewModel.type('\n', 'keyboard'); - assert.deepEqual(model.getLineContent(11), ' ]'); + assert.deepStrictEqual(model.getLineContent(11), ' ]'); }); model.dispose(); mode.dispose(); }); + + test('issue #111128: Multicursor `Enter` issue with indentation', () => { + const model = createTextModel(' let a, b, c;', { detectIndentation: false, insertSpaces: false, tabSize: 4 }, mode.getLanguageIdentifier()); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + editor.setSelections([ + new Selection(1, 11, 1, 11), + new Selection(1, 14, 1, 14), + ]); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), ' let a,\n\t b,\n\t c;'); + }); + }); }); interface ICursorOpts { @@ -4218,7 +4333,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('*', 'keyboard'); - assert.deepEqual(model.getLineContent(2), '*'); + assert.deepStrictEqual(model.getLineContent(2), '*'); }); mode.dispose(); }); @@ -4234,7 +4349,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' }'); + assert.deepStrictEqual(model.getLineContent(2), ' }'); }); mode.dispose(); }); @@ -4250,7 +4365,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 5); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' }'); + assert.deepStrictEqual(model.getLineContent(2), ' }'); }); mode.dispose(); }); @@ -4268,7 +4383,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 4, 1); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(4), ' } '); + assert.deepStrictEqual(model.getLineContent(4), ' } '); }); mode.dispose(); }); @@ -4286,7 +4401,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 4, 6); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(4), ' } }'); + assert.deepStrictEqual(model.getLineContent(4), ' } }'); }); mode.dispose(); }); @@ -4302,7 +4417,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' }// hello'); + assert.deepStrictEqual(model.getLineContent(2), ' }// hello'); }); mode.dispose(); }); @@ -4318,7 +4433,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 3); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' }'); + assert.deepStrictEqual(model.getLineContent(2), ' }'); }); mode.dispose(); }); @@ -4334,7 +4449,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 2); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), 'a}'); + assert.deepStrictEqual(model.getLineContent(2), 'a}'); }); mode.dispose(); }); @@ -4351,7 +4466,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 13); viewModel.type('*', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' ( 1 + 2 ) *'); + assert.deepStrictEqual(model.getLineContent(2), ' ( 1 + 2 ) *'); }); mode.dispose(); }); @@ -4370,8 +4485,8 @@ suite('ElectricCharacter', () => { changeText = e.changes[0].text; }); viewModel.type(')', 'keyboard'); - assert.deepEqual(model.getLineContent(1), '(div)'); - assert.deepEqual(changeText, ')'); + assert.deepStrictEqual(model.getLineContent(1), '(div)'); + assert.deepStrictEqual(changeText, ')'); }); mode.dispose(); }); @@ -4388,7 +4503,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 3); viewModel.type(')', 'keyboard'); - assert.deepEqual(model.getLineContent(3), '\t3)'); + assert.deepStrictEqual(model.getLineContent(3), '\t3)'); }); mode.dispose(); }); @@ -4404,7 +4519,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 3); viewModel.type('*', 'keyboard'); - assert.deepEqual(model.getLineContent(2), '/** */'); + assert.deepStrictEqual(model.getLineContent(2), '/** */'); }); mode.dispose(); }); @@ -4420,7 +4535,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 5); viewModel.type('*', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' /** */'); + assert.deepStrictEqual(model.getLineContent(2), ' /** */'); }); mode.dispose(); }); @@ -4437,7 +4552,7 @@ suite('ElectricCharacter', () => { moveTo(editor, viewModel, 2, 5); moveTo(editor, viewModel, 2, 1, true); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), '}'); + assert.deepStrictEqual(model.getLineContent(2), '}'); }); mode.dispose(); }); @@ -4513,7 +4628,7 @@ suite('autoClosingPairs', () => { let expected = lineContent.substr(0, column - 1) + expectedInsert + lineContent.substr(column - 1); moveTo(editor, viewModel, lineNumber, column); viewModel.type(chr, 'keyboard'); - assert.deepEqual(model.getLineContent(lineNumber), expected, message); + assert.deepStrictEqual(model.getLineContent(lineNumber), expected, message); model.undo(); } @@ -4783,12 +4898,12 @@ suite('autoClosingPairs', () => { // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), '`var` a = `asd`'); + assert.strictEqual(model.getValue(), '`var` a = `asd`'); // type a ( viewModel.type('(', 'keyboard'); - assert.equal(model.getValue(), '`(var)` a = `(asd)`'); + assert.strictEqual(model.getValue(), '`(var)` a = `(asd)`'); }); usingCursor({ @@ -4808,7 +4923,7 @@ suite('autoClosingPairs', () => { // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), '` a = asd'); + assert.strictEqual(model.getValue(), '` a = asd'); }); usingCursor({ @@ -4827,11 +4942,11 @@ suite('autoClosingPairs', () => { // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), '`var` a = asd'); + assert.strictEqual(model.getValue(), '`var` a = asd'); // type a ( viewModel.type('(', 'keyboard'); - assert.equal(model.getValue(), '`(` a = asd'); + assert.strictEqual(model.getValue(), '`(` a = asd'); }); usingCursor({ @@ -4850,11 +4965,11 @@ suite('autoClosingPairs', () => { // type a ( viewModel.type('(', 'keyboard'); - assert.equal(model.getValue(), '(var) a = asd'); + assert.strictEqual(model.getValue(), '(var) a = asd'); // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), '(`) a = asd'); + assert.strictEqual(model.getValue(), '(`) a = asd'); }); mode.dispose(); }); @@ -5047,50 +5162,50 @@ suite('autoClosingPairs', () => { // First gif model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste1 = teste\' ok'); - assert.equal(model.getLineContent(1), 'teste1 = teste\' ok'); + assert.strictEqual(model.getLineContent(1), 'teste1 = teste\' ok'); viewModel.setSelections('test', [new Selection(1, 1000, 1, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste2 = teste \'ok'); - assert.equal(model.getLineContent(2), 'teste2 = teste \'ok\''); + assert.strictEqual(model.getLineContent(2), 'teste2 = teste \'ok\''); viewModel.setSelections('test', [new Selection(2, 1000, 2, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste3 = teste" ok'); - assert.equal(model.getLineContent(3), 'teste3 = teste" ok'); + assert.strictEqual(model.getLineContent(3), 'teste3 = teste" ok'); viewModel.setSelections('test', [new Selection(3, 1000, 3, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste4 = teste "ok'); - assert.equal(model.getLineContent(4), 'teste4 = teste "ok"'); + assert.strictEqual(model.getLineContent(4), 'teste4 = teste "ok"'); // Second gif viewModel.setSelections('test', [new Selection(4, 1000, 4, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste \''); - assert.equal(model.getLineContent(5), 'teste \'\''); + assert.strictEqual(model.getLineContent(5), 'teste \'\''); viewModel.setSelections('test', [new Selection(5, 1000, 5, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste "'); - assert.equal(model.getLineContent(6), 'teste ""'); + assert.strictEqual(model.getLineContent(6), 'teste ""'); viewModel.setSelections('test', [new Selection(6, 1000, 6, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste\''); - assert.equal(model.getLineContent(7), 'teste\''); + assert.strictEqual(model.getLineContent(7), 'teste\''); viewModel.setSelections('test', [new Selection(7, 1000, 7, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste"'); - assert.equal(model.getLineContent(8), 'teste"'); + assert.strictEqual(model.getLineContent(8), 'teste"'); }); mode.dispose(); }); @@ -5337,7 +5452,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('è', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), 'è'); + assert.strictEqual(model.getValue(), 'è'); }); mode.dispose(); }); @@ -5359,7 +5474,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'test\''); + assert.strictEqual(model.getValue(), '\'test\''); }); mode.dispose(); }); @@ -5376,16 +5491,16 @@ suite('autoClosingPairs', () => { viewModel.setSelections('test', [new Selection(1, 13, 1, 13)]); viewModel.type('\'', 'keyboard'); - assert.equal(model.getValue(), 'console.log(\'\');'); + assert.strictEqual(model.getValue(), 'console.log(\'\');'); viewModel.type('it', 'keyboard'); - assert.equal(model.getValue(), 'console.log(\'it\');'); + assert.strictEqual(model.getValue(), 'console.log(\'it\');'); viewModel.type('\\', 'keyboard'); - assert.equal(model.getValue(), 'console.log(\'it\\\');'); + assert.strictEqual(model.getValue(), 'console.log(\'it\\\');'); viewModel.type('\'', 'keyboard'); - assert.equal(model.getValue(), 'console.log(\'it\\\'\');'); + assert.strictEqual(model.getValue(), 'console.log(\'it\\\'\');'); }); mode.dispose(); }); @@ -5402,19 +5517,19 @@ suite('autoClosingPairs', () => { viewModel.setSelections('test', [new Selection(1, 1, 1, 1)]); viewModel.type('\\', 'keyboard'); - assert.equal(model.getValue(), '\\'); + assert.strictEqual(model.getValue(), '\\'); viewModel.type('(', 'keyboard'); - assert.equal(model.getValue(), '\\()'); + assert.strictEqual(model.getValue(), '\\()'); viewModel.type('abc', 'keyboard'); - assert.equal(model.getValue(), '\\(abc)'); + assert.strictEqual(model.getValue(), '\\(abc)'); viewModel.type('\\', 'keyboard'); - assert.equal(model.getValue(), '\\(abc\\)'); + assert.strictEqual(model.getValue(), '\\(abc\\)'); viewModel.type(')', 'keyboard'); - assert.equal(model.getValue(), '\\(abc\\)'); + assert.strictEqual(model.getValue(), '\\(abc\\)'); }); mode.dispose(); }); @@ -5439,7 +5554,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('`', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '`hello\nworld'); + assert.strictEqual(model.getValue(), '`hello\nworld'); assertCursor(viewModel, new Selection(1, 2, 2, 2)); }); mode.dispose(); @@ -5462,14 +5577,14 @@ suite('autoClosingPairs', () => { viewModel.type('\'', 'keyboard'); viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'\''); + assert.strictEqual(model.getValue(), '\'\''); // Typing one more ' + space viewModel.startComposition(); viewModel.type('\'', 'keyboard'); viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'\''); + assert.strictEqual(model.getValue(), '\'\''); // Typing ' as a closing tag model.setValue('\'abc'); @@ -5479,7 +5594,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'abc\''); + assert.strictEqual(model.getValue(), '\'abc\''); // quotes before the newly added character are all paired. model.setValue('\'abc\'def '); @@ -5489,7 +5604,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'abc\'def \'\''); + assert.strictEqual(model.getValue(), '\'abc\'def \'\''); // No auto closing if there is non-whitespace character after the cursor model.setValue('abc'); @@ -5507,7 +5622,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), 'abc\''); + assert.strictEqual(model.getValue(), 'abc\''); }); mode.dispose(); }); @@ -5527,7 +5642,7 @@ suite('autoClosingPairs', () => { viewModel.type('a', 'keyboard'); viewModel.replacePreviousChar('', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '{}'); + assert.strictEqual(model.getValue(), '{}'); }); mode.dispose(); }); @@ -5549,7 +5664,7 @@ suite('autoClosingPairs', () => { // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), 'var a = `asd`'); + assert.strictEqual(model.getValue(), 'var a = `asd`'); }); mode.dispose(); }); @@ -5577,14 +5692,14 @@ suite('autoClosingPairs', () => { new Selection(1, 12, 1, 13) ]); viewModel.type('"', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), 'var x = "hi";', 'assert1'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'var x = "hi";', 'assert1'); editor.setSelections([ new Selection(1, 9, 1, 10), new Selection(1, 12, 1, 13) ]); viewModel.type('\'', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), 'var x = \'hi\';', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'var x = \'hi\';', 'assert2'); }); model.dispose(); @@ -5610,7 +5725,7 @@ suite('autoClosingPairs', () => { // delete left CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), 'va a = )'); + assert.strictEqual(model.getValue(), 'va a = )'); }); model.dispose(); mode.dispose(); @@ -5657,20 +5772,20 @@ suite('Undo stops', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); viewModel.type('first', 'keyboard'); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A fir line'); + assert.strictEqual(model.getLineContent(1), 'A fir line'); assertCursor(viewModel, new Selection(1, 6, 1, 6)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A line'); + assert.strictEqual(model.getLineContent(1), 'A line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5686,20 +5801,20 @@ suite('Undo stops', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); viewModel.type('first', 'keyboard'); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A firstine'); + assert.strictEqual(model.getLineContent(1), 'A firstine'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A line'); + assert.strictEqual(model.getLineContent(1), 'A line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5721,19 +5836,19 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' line'); + assert.strictEqual(model.getLineContent(2), ' line'); assertCursor(viewModel, new Selection(2, 1, 2, 1)); viewModel.type('Second', 'keyboard'); - assert.equal(model.getLineContent(2), 'Second line'); + assert.strictEqual(model.getLineContent(2), 'Second line'); assertCursor(viewModel, new Selection(2, 7, 2, 7)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' line'); + assert.strictEqual(model.getLineContent(2), ' line'); assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another line'); + assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 8, 2, 8)); }); }); @@ -5755,7 +5870,7 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' line'); + assert.strictEqual(model.getLineContent(2), ' line'); assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); @@ -5763,15 +5878,15 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(2), ''); assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' line'); + assert.strictEqual(model.getLineContent(2), ' line'); assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another line'); + assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 8, 2, 8)); }); }); @@ -5790,19 +5905,19 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another '); + assert.strictEqual(model.getLineContent(2), 'Another '); assertCursor(viewModel, new Selection(2, 9, 2, 9)); viewModel.type('text', 'keyboard'); - assert.equal(model.getLineContent(2), 'Another text'); + assert.strictEqual(model.getLineContent(2), 'Another text'); assertCursor(viewModel, new Selection(2, 13, 2, 13)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another '); + assert.strictEqual(model.getLineContent(2), 'Another '); assertCursor(viewModel, new Selection(2, 9, 2, 9)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another line'); + assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 9, 2, 9)); }); }); @@ -5821,7 +5936,7 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another '); + assert.strictEqual(model.getLineContent(2), 'Another '); assertCursor(viewModel, new Selection(2, 9, 2, 9)); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); @@ -5830,15 +5945,15 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'An'); + assert.strictEqual(model.getLineContent(2), 'An'); assertCursor(viewModel, new Selection(2, 3, 2, 3)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another '); + assert.strictEqual(model.getLineContent(2), 'Another '); assertCursor(viewModel, new Selection(2, 9, 2, 9)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another line'); + assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 9, 2, 9)); }); }); @@ -5854,19 +5969,19 @@ suite('Undo stops', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); viewModel.type('first and interesting', 'keyboard'); - assert.equal(model.getLineContent(1), 'A first and interesting line'); + assert.strictEqual(model.getLineContent(1), 'A first and interesting line'); assertCursor(viewModel, new Selection(1, 24, 1, 24)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A first and line'); + assert.strictEqual(model.getLineContent(1), 'A first and line'); assertCursor(viewModel, new Selection(1, 12, 1, 12)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A line'); + assert.strictEqual(model.getLineContent(1), 'A line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5882,15 +5997,15 @@ suite('Undo stops', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); viewModel.type('first', 'keyboard'); - assert.equal(model.getValue(), 'A first line\nAnother line'); + assert.strictEqual(model.getValue(), 'A first line\nAnother line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); model.pushEOL(EndOfLineSequence.CRLF); - assert.equal(model.getValue(), 'A first line\r\nAnother line'); + assert.strictEqual(model.getValue(), 'A first line\r\nAnother line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), 'A line\nAnother line'); + assert.strictEqual(model.getValue(), 'A line\nAnother line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5909,10 +6024,10 @@ suite('Undo stops', () => { new Selection(1, 7, 1, 12), ]); viewModel.type('no', 'keyboard'); - assert.equal(model.getValue(), 'hello no\nhello no'); + assert.strictEqual(model.getValue(), 'hello no\nhello no'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), 'hello world\nhello world'); + assert.strictEqual(model.getValue(), 'hello world\nhello world'); }); }); }); diff --git a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts index 3c85cc22e..55abff9fa 100644 --- a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts +++ b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts @@ -484,11 +484,11 @@ function cursorEqual(viewModel: ViewModel, posLineNumber: number, posColumn: num } function positionEqual(position: Position, lineNumber: number, column: number) { - assert.deepEqual(position, new Position(lineNumber, column), 'position equal'); + assert.deepStrictEqual(position, new Position(lineNumber, column), 'position equal'); } function selectionEqual(selection: Selection, posLineNumber: number, posColumn: number, selLineNumber: number, selColumn: number) { - assert.deepEqual({ + assert.deepStrictEqual({ selectionStartLineNumber: selection.selectionStartLineNumber, selectionStartColumn: selection.selectionStartColumn, positionLineNumber: selection.positionLineNumber, diff --git a/src/vs/editor/test/browser/controller/textAreaState.test.ts b/src/vs/editor/test/browser/controller/textAreaState.test.ts index 163e9de54..4ae25e14a 100644 --- a/src/vs/editor/test/browser/controller/textAreaState.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaState.test.ts @@ -84,8 +84,8 @@ suite('TextAreaState', () => { let actual = TextAreaState.readFromTextArea(textArea); assertTextAreaState(actual, 'Hello world!', 1, 12); - assert.equal(actual.value, 'Hello world!'); - assert.equal(actual.selectionStart, 1); + assert.strictEqual(actual.value, 'Hello world!'); + assert.strictEqual(actual.selectionStart, 1); actual = actual.collapseSelection(); assertTextAreaState(actual, 'Hello world!', 12, 12); @@ -102,23 +102,23 @@ suite('TextAreaState', () => { let state = new TextAreaState('Hi world!', 2, 2, null, null); state.writeToTextArea('test', textArea, false); - assert.equal(textArea._value, 'Hi world!'); - assert.equal(textArea._selectionStart, 9); - assert.equal(textArea._selectionEnd, 9); + assert.strictEqual(textArea._value, 'Hi world!'); + assert.strictEqual(textArea._selectionStart, 9); + assert.strictEqual(textArea._selectionEnd, 9); state = new TextAreaState('Hi world!', 3, 3, null, null); state.writeToTextArea('test', textArea, false); - assert.equal(textArea._value, 'Hi world!'); - assert.equal(textArea._selectionStart, 9); - assert.equal(textArea._selectionEnd, 9); + assert.strictEqual(textArea._value, 'Hi world!'); + assert.strictEqual(textArea._selectionStart, 9); + assert.strictEqual(textArea._selectionEnd, 9); state = new TextAreaState('Hi world!', 0, 2, null, null); state.writeToTextArea('test', textArea, true); - assert.equal(textArea._value, 'Hi world!'); - assert.equal(textArea._selectionStart, 0); - assert.equal(textArea._selectionEnd, 2); + assert.strictEqual(textArea._value, 'Hi world!'); + assert.strictEqual(textArea._selectionStart, 0); + assert.strictEqual(textArea._selectionEnd, 2); textArea.dispose(); }); @@ -134,8 +134,8 @@ suite('TextAreaState', () => { let newState = TextAreaState.readFromTextArea(textArea); let actual = TextAreaState.deduceInput(prevState, newState, couldBeEmojiInput); - assert.equal(actual.text, expected); - assert.equal(actual.replaceCharCnt, expectedCharReplaceCnt); + assert.strictEqual(actual.text, expected); + assert.strictEqual(actual.replaceCharCnt, expectedCharReplaceCnt); textArea.dispose(); } diff --git a/src/vs/editor/test/browser/core/editorState.test.ts b/src/vs/editor/test/browser/core/editorState.test.ts index 1915af4f7..32534c2b7 100644 --- a/src/vs/editor/test/browser/core/editorState.test.ts +++ b/src/vs/editor/test/browser/core/editorState.test.ts @@ -29,7 +29,7 @@ suite('Editor Core - Editor State', () => { test('empty editor state should be valid', () => { let result = validate({}, {}); - assert.equal(result, true); + assert.strictEqual(result, true); }); test('different model URIs should be invalid', () => { @@ -38,7 +38,7 @@ suite('Editor Core - Editor State', () => { { model: { uri: URI.parse('http://test2') } } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); test('different model versions should be invalid', () => { @@ -47,7 +47,7 @@ suite('Editor Core - Editor State', () => { { model: { version: 2 } } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); test('different positions should be invalid', () => { @@ -56,7 +56,7 @@ suite('Editor Core - Editor State', () => { { position: new Position(2, 3) } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); test('different selections should be invalid', () => { @@ -65,7 +65,7 @@ suite('Editor Core - Editor State', () => { { selection: new Selection(5, 2, 3, 4) } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); test('different scroll positions should be invalid', () => { @@ -74,7 +74,7 @@ suite('Editor Core - Editor State', () => { { scroll: { left: 3, top: 2 } } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); diff --git a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts index 9c8c6c6ed..37055a854 100644 --- a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts +++ b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts @@ -60,12 +60,12 @@ suite('Decoration Render Options', () => { test('register and resolve decoration type', () => { let s = new TestCodeEditorServiceImpl(themeServiceMock); s.registerDecorationType('example', options); - assert.notEqual(s.resolveDecorationOptions('example', false), undefined); + assert.notStrictEqual(s.resolveDecorationOptions('example', false), undefined); }); test('remove decoration type', () => { let s = new TestCodeEditorServiceImpl(themeServiceMock); s.registerDecorationType('example', options); - assert.notEqual(s.resolveDecorationOptions('example', false), undefined); + assert.notStrictEqual(s.resolveDecorationOptions('example', false), undefined); s.removeDecorationType('example'); assert.throws(() => s.resolveDecorationOptions('example', false)); }); @@ -95,16 +95,16 @@ suite('Decoration Render Options', () => { })); const s = new TestCodeEditorServiceImpl(themeService, styleSheet); s.registerDecorationType('example', options); - assert.equal(readStyleSheet(styleSheet), '.monaco-editor .ced-example-0 {background-color:#ff0000;border-color:transparent;box-sizing: border-box;}'); + assert.strictEqual(readStyleSheet(styleSheet), '.monaco-editor .ced-example-0 {background-color:#ff0000;border-color:transparent;box-sizing: border-box;}'); themeService.setTheme(new TestColorTheme({ editorBackground: '#EE0000', editorBorder: '#00FFFF' })); - assert.equal(readStyleSheet(styleSheet), '.monaco-editor .ced-example-0 {background-color:#ee0000;border-color:#00ffff;box-sizing: border-box;}'); + assert.strictEqual(readStyleSheet(styleSheet), '.monaco-editor .ced-example-0 {background-color:#ee0000;border-color:#00ffff;box-sizing: border-box;}'); s.removeDecorationType('example'); - assert.equal(readStyleSheet(styleSheet), ''); + assert.strictEqual(readStyleSheet(styleSheet), ''); }); test('theme overrides', () => { @@ -134,10 +134,10 @@ suite('Decoration Render Options', () => { '.vs.monaco-editor .ced-example-1 {color:#FF00FF !important;}', '.monaco-editor .ced-example-1 {color:#ff0000 !important;}' ].join('\n'); - assert.equal(readStyleSheet(styleSheet), expected); + assert.strictEqual(readStyleSheet(styleSheet), expected); s.removeDecorationType('example'); - assert.equal(readStyleSheet(styleSheet), ''); + assert.strictEqual(readStyleSheet(styleSheet), ''); }); test('css properties, gutterIconPaths', () => { diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index cc39f3044..da4aac219 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -31,10 +31,10 @@ export interface ITestCodeEditor extends IActiveCodeEditor { registerAndInstantiateContribution(id: string, ctor: new (editor: ICodeEditor, ...services: Services) => T): T; } -class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { +export class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { //#region testing overrides - protected _createConfiguration(options: IEditorConstructionOptions): IConfiguration { + protected _createConfiguration(options: Readonly): IConfiguration { return new TestConfiguration(options); } protected _createView(viewModel: ViewModel): [View, boolean] { @@ -52,6 +52,9 @@ class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { this._contributions[id] = r; return r; } +} + +class TestCodeEditorWithAutoModelDisposal extends TestCodeEditor { public dispose() { super.dispose(); if (this._modelData) { @@ -144,7 +147,7 @@ export function createTestCodeEditor(options: TestCodeEditorCreationOptions): IT contributions: [] }; const editor = instantiationService.createInstance( - TestCodeEditor, + TestCodeEditorWithAutoModelDisposal, new TestEditorDomElement(), options, codeEditorWidgetOptions diff --git a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts index cde867901..81aed990b 100644 --- a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts +++ b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts @@ -85,7 +85,7 @@ suite('MinimapCharRenderer', () => { actual[i] = imageData.data[i]; } - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ 0x2D, 0x2D, 0x2D, 0xFF, 0xAC, 0xAC, 0xAC, 0xFF, 0xC6, 0xC6, 0xC6, 0xFF, 0xC8, 0xC8, 0xC8, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xCB, 0xCB, 0xCB, 0xFF, @@ -115,7 +115,7 @@ suite('MinimapCharRenderer', () => { actual[i] = imageData.data[i]; } - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ 0xCB, 0xCB, 0xCB, 0xFF, 0x81, 0x81, 0x81, 0xFF, ]); diff --git a/src/vs/editor/test/browser/view/viewLayer.test.ts b/src/vs/editor/test/browser/view/viewLayer.test.ts index bffb2f8b1..eba81ff74 100644 --- a/src/vs/editor/test/browser/view/viewLayer.test.ts +++ b/src/vs/editor/test/browser/view/viewLayer.test.ts @@ -36,7 +36,7 @@ function assertState(col: RenderedLinesCollection, state: ILinesCollec actualState.lines.push(col.getLine(lineNumber).id); actualState.pinged.push(col.getLine(lineNumber)._pinged); } - assert.deepEqual(actualState, state); + assert.deepStrictEqual(actualState, state); } suite('RenderedLinesCollection onLinesDeleted', () => { @@ -54,7 +54,7 @@ suite('RenderedLinesCollection onLinesDeleted', () => { if (actualDeleted1) { actualDeleted = actualDeleted1.map(line => line.id); } - assert.deepEqual(actualDeleted, expectedDeleted); + assert.deepStrictEqual(actualDeleted, expectedDeleted); assertState(col, expectedState); } @@ -325,7 +325,7 @@ suite('RenderedLinesCollection onLineChanged', () => { new TestLine('old9') ]); let actualPinged = col.onLinesChanged(changedLineNumber, changedLineNumber); - assert.deepEqual(actualPinged, expectedPinged); + assert.deepStrictEqual(actualPinged, expectedPinged); assertState(col, expectedState); } @@ -410,7 +410,7 @@ suite('RenderedLinesCollection onLinesInserted', () => { if (actualDeleted1) { actualDeleted = actualDeleted1.map(line => line.id); } - assert.deepEqual(actualDeleted, expectedDeleted); + assert.deepStrictEqual(actualDeleted, expectedDeleted); assertState(col, expectedState); } @@ -682,7 +682,7 @@ suite('RenderedLinesCollection onTokensChanged', () => { new TestLine('old9') ]); let actualPinged = col.onTokensChanged([{ fromLineNumber: changedFromLineNumber, toLineNumber: changedToLineNumber }]); - assert.deepEqual(actualPinged, expectedPinged); + assert.deepStrictEqual(actualPinged, expectedPinged); assertState(col, expectedState); } diff --git a/src/vs/editor/test/common/config/commonEditorConfig.test.ts b/src/vs/editor/test/common/config/commonEditorConfig.test.ts index d0edc8f44..1faca7a17 100644 --- a/src/vs/editor/test/common/config/commonEditorConfig.test.ts +++ b/src/vs/editor/test/common/config/commonEditorConfig.test.ts @@ -16,40 +16,40 @@ suite('Common Editor Config', () => { const zoom = EditorZoom; zoom.setZoomLevel(0); - assert.equal(zoom.getZoomLevel(), 0); + assert.strictEqual(zoom.getZoomLevel(), 0); zoom.setZoomLevel(-0); - assert.equal(zoom.getZoomLevel(), 0); + assert.strictEqual(zoom.getZoomLevel(), 0); zoom.setZoomLevel(5); - assert.equal(zoom.getZoomLevel(), 5); + assert.strictEqual(zoom.getZoomLevel(), 5); zoom.setZoomLevel(-1); - assert.equal(zoom.getZoomLevel(), -1); + assert.strictEqual(zoom.getZoomLevel(), -1); zoom.setZoomLevel(9); - assert.equal(zoom.getZoomLevel(), 9); + assert.strictEqual(zoom.getZoomLevel(), 9); zoom.setZoomLevel(-9); - assert.equal(zoom.getZoomLevel(), -5); + assert.strictEqual(zoom.getZoomLevel(), -5); zoom.setZoomLevel(20); - assert.equal(zoom.getZoomLevel(), 20); + assert.strictEqual(zoom.getZoomLevel(), 20); zoom.setZoomLevel(-10); - assert.equal(zoom.getZoomLevel(), -5); + assert.strictEqual(zoom.getZoomLevel(), -5); zoom.setZoomLevel(9.1); - assert.equal(zoom.getZoomLevel(), 9.1); + assert.strictEqual(zoom.getZoomLevel(), 9.1); zoom.setZoomLevel(-9.1); - assert.equal(zoom.getZoomLevel(), -5); + assert.strictEqual(zoom.getZoomLevel(), -5); zoom.setZoomLevel(Infinity); - assert.equal(zoom.getZoomLevel(), 20); + assert.strictEqual(zoom.getZoomLevel(), 20); zoom.setZoomLevel(Number.NEGATIVE_INFINITY); - assert.equal(zoom.getZoomLevel(), -5); + assert.strictEqual(zoom.getZoomLevel(), -5); }); class TestWrappingConfiguration extends TestConfiguration { @@ -69,8 +69,8 @@ suite('Common Editor Config', () => { function assertWrapping(config: TestConfiguration, isViewportWrapping: boolean, wrappingColumn: number): void { const options = config.options; const wrappingInfo = options.get(EditorOption.wrappingInfo); - assert.equal(wrappingInfo.isViewportWrapping, isViewportWrapping); - assert.equal(wrappingInfo.wrappingColumn, wrappingColumn); + assert.strictEqual(wrappingInfo.isViewportWrapping, isViewportWrapping); + assert.strictEqual(wrappingInfo.wrappingColumn, wrappingColumn); } test('wordWrap default', () => { @@ -186,26 +186,26 @@ suite('Common Editor Config', () => { }); let config = new TestConfiguration({ hover: hoverOptions }); - assert.equal(config.options.get(EditorOption.hover).enabled, true); + assert.strictEqual(config.options.get(EditorOption.hover).enabled, true); config.updateOptions({ hover: { enabled: false } }); - assert.equal(config.options.get(EditorOption.hover).enabled, false); + assert.strictEqual(config.options.get(EditorOption.hover).enabled, false); }); test('does not emit event when nothing changes', () => { const config = new TestConfiguration({ glyphMargin: true, roundedSelection: false }); let event: ConfigurationChangedEvent | null = null; config.onDidChange(e => event = e); - assert.equal(config.options.get(EditorOption.glyphMargin), true); + assert.strictEqual(config.options.get(EditorOption.glyphMargin), true); config.updateOptions({ glyphMargin: true }); config.updateOptions({ roundedSelection: false }); - assert.equal(event, null); + assert.strictEqual(event, null); }); test('issue #94931: Unable to open source file', () => { const config = new TestConfiguration({ quickSuggestions: null! }); const actual = >>config.options.get(EditorOption.quickSuggestions); - assert.deepEqual(actual, { + assert.deepStrictEqual(actual, { other: true, comments: false, strings: false @@ -216,7 +216,7 @@ suite('Common Editor Config', () => { const config = new TestConfiguration({ quickSuggestions: null! }); config.updateOptions({ quickSuggestions: { strings: true } }); const actual = >>config.options.get(EditorOption.quickSuggestions); - assert.deepEqual(actual, { + assert.deepStrictEqual(actual, { other: true, comments: false, strings: true diff --git a/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts b/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts index e7cd6b4b3..e07042281 100644 --- a/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts +++ b/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts @@ -8,41 +8,41 @@ import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; suite('CursorMove', () => { test('nextRenderTabStop', () => { - assert.equal(CursorColumns.nextRenderTabStop(0, 4), 4); - assert.equal(CursorColumns.nextRenderTabStop(1, 4), 4); - assert.equal(CursorColumns.nextRenderTabStop(2, 4), 4); - assert.equal(CursorColumns.nextRenderTabStop(3, 4), 4); - assert.equal(CursorColumns.nextRenderTabStop(4, 4), 8); - assert.equal(CursorColumns.nextRenderTabStop(5, 4), 8); - assert.equal(CursorColumns.nextRenderTabStop(6, 4), 8); - assert.equal(CursorColumns.nextRenderTabStop(7, 4), 8); - assert.equal(CursorColumns.nextRenderTabStop(8, 4), 12); + assert.strictEqual(CursorColumns.nextRenderTabStop(0, 4), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(1, 4), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(2, 4), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(3, 4), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(4, 4), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(5, 4), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(6, 4), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(7, 4), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(8, 4), 12); - assert.equal(CursorColumns.nextRenderTabStop(0, 2), 2); - assert.equal(CursorColumns.nextRenderTabStop(1, 2), 2); - assert.equal(CursorColumns.nextRenderTabStop(2, 2), 4); - assert.equal(CursorColumns.nextRenderTabStop(3, 2), 4); - assert.equal(CursorColumns.nextRenderTabStop(4, 2), 6); - assert.equal(CursorColumns.nextRenderTabStop(5, 2), 6); - assert.equal(CursorColumns.nextRenderTabStop(6, 2), 8); - assert.equal(CursorColumns.nextRenderTabStop(7, 2), 8); - assert.equal(CursorColumns.nextRenderTabStop(8, 2), 10); + assert.strictEqual(CursorColumns.nextRenderTabStop(0, 2), 2); + assert.strictEqual(CursorColumns.nextRenderTabStop(1, 2), 2); + assert.strictEqual(CursorColumns.nextRenderTabStop(2, 2), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(3, 2), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(4, 2), 6); + assert.strictEqual(CursorColumns.nextRenderTabStop(5, 2), 6); + assert.strictEqual(CursorColumns.nextRenderTabStop(6, 2), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(7, 2), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(8, 2), 10); - assert.equal(CursorColumns.nextRenderTabStop(0, 1), 1); - assert.equal(CursorColumns.nextRenderTabStop(1, 1), 2); - assert.equal(CursorColumns.nextRenderTabStop(2, 1), 3); - assert.equal(CursorColumns.nextRenderTabStop(3, 1), 4); - assert.equal(CursorColumns.nextRenderTabStop(4, 1), 5); - assert.equal(CursorColumns.nextRenderTabStop(5, 1), 6); - assert.equal(CursorColumns.nextRenderTabStop(6, 1), 7); - assert.equal(CursorColumns.nextRenderTabStop(7, 1), 8); - assert.equal(CursorColumns.nextRenderTabStop(8, 1), 9); + assert.strictEqual(CursorColumns.nextRenderTabStop(0, 1), 1); + assert.strictEqual(CursorColumns.nextRenderTabStop(1, 1), 2); + assert.strictEqual(CursorColumns.nextRenderTabStop(2, 1), 3); + assert.strictEqual(CursorColumns.nextRenderTabStop(3, 1), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(4, 1), 5); + assert.strictEqual(CursorColumns.nextRenderTabStop(5, 1), 6); + assert.strictEqual(CursorColumns.nextRenderTabStop(6, 1), 7); + assert.strictEqual(CursorColumns.nextRenderTabStop(7, 1), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(8, 1), 9); }); test('visibleColumnFromColumn', () => { function testVisibleColumnFromColumn(text: string, tabSize: number, column: number, expected: number): void { - assert.equal(CursorColumns.visibleColumnFromColumn(text, column, tabSize), expected); + assert.strictEqual(CursorColumns.visibleColumnFromColumn(text, column, tabSize), expected); } testVisibleColumnFromColumn('\t\tvar x = 3;', 4, 1, 0); @@ -101,7 +101,7 @@ suite('CursorMove', () => { test('columnFromVisibleColumn', () => { function testColumnFromVisibleColumn(text: string, tabSize: number, visibleColumn: number, expected: number): void { - assert.equal(CursorColumns.columnFromVisibleColumn(text, visibleColumn, tabSize), expected); + assert.strictEqual(CursorColumns.columnFromVisibleColumn(text, visibleColumn, tabSize), expected); } // testColumnFromVisibleColumn('\t\tvar x = 3;', 4, 0, 1); @@ -177,7 +177,7 @@ suite('CursorMove', () => { test('toStatusbarColumn', () => { function t(text: string, tabSize: number, column: number, expected: number): void { - assert.equal(CursorColumns.toStatusbarColumn(text, column, tabSize), expected, `<>`); + assert.strictEqual(CursorColumns.toStatusbarColumn(text, column, tabSize), expected, `<>`); } t(' spaces', 4, 1, 1); diff --git a/src/vs/editor/test/common/core/characterClassifier.test.ts b/src/vs/editor/test/common/core/characterClassifier.test.ts index de9effc74..9d3bf750c 100644 --- a/src/vs/editor/test/common/core/characterClassifier.test.ts +++ b/src/vs/editor/test/common/core/characterClassifier.test.ts @@ -11,27 +11,27 @@ suite('CharacterClassifier', () => { test('works', () => { let classifier = new CharacterClassifier(0); - assert.equal(classifier.get(-1), 0); - assert.equal(classifier.get(0), 0); - assert.equal(classifier.get(CharCode.a), 0); - assert.equal(classifier.get(CharCode.b), 0); - assert.equal(classifier.get(CharCode.z), 0); - assert.equal(classifier.get(255), 0); - assert.equal(classifier.get(1000), 0); - assert.equal(classifier.get(2000), 0); + assert.strictEqual(classifier.get(-1), 0); + assert.strictEqual(classifier.get(0), 0); + assert.strictEqual(classifier.get(CharCode.a), 0); + assert.strictEqual(classifier.get(CharCode.b), 0); + assert.strictEqual(classifier.get(CharCode.z), 0); + assert.strictEqual(classifier.get(255), 0); + assert.strictEqual(classifier.get(1000), 0); + assert.strictEqual(classifier.get(2000), 0); classifier.set(CharCode.a, 1); classifier.set(CharCode.z, 2); classifier.set(1000, 3); - assert.equal(classifier.get(-1), 0); - assert.equal(classifier.get(0), 0); - assert.equal(classifier.get(CharCode.a), 1); - assert.equal(classifier.get(CharCode.b), 0); - assert.equal(classifier.get(CharCode.z), 2); - assert.equal(classifier.get(255), 0); - assert.equal(classifier.get(1000), 3); - assert.equal(classifier.get(2000), 0); + assert.strictEqual(classifier.get(-1), 0); + assert.strictEqual(classifier.get(0), 0); + assert.strictEqual(classifier.get(CharCode.a), 1); + assert.strictEqual(classifier.get(CharCode.b), 0); + assert.strictEqual(classifier.get(CharCode.z), 2); + assert.strictEqual(classifier.get(255), 0); + assert.strictEqual(classifier.get(1000), 3); + assert.strictEqual(classifier.get(2000), 0); }); -}); \ No newline at end of file +}); diff --git a/src/vs/editor/test/common/core/lineTokens.test.ts b/src/vs/editor/test/common/core/lineTokens.test.ts index d3a9924fb..2ffff0c7e 100644 --- a/src/vs/editor/test/common/core/lineTokens.test.ts +++ b/src/vs/editor/test/common/core/lineTokens.test.ts @@ -45,64 +45,64 @@ suite('LineTokens', () => { test('basics', () => { const lineTokens = createTestLineTokens(); - assert.equal(lineTokens.getLineContent(), 'Hello world, this is a lovely day'); - assert.equal(lineTokens.getLineContent().length, 33); - assert.equal(lineTokens.getCount(), 7); + assert.strictEqual(lineTokens.getLineContent(), 'Hello world, this is a lovely day'); + assert.strictEqual(lineTokens.getLineContent().length, 33); + assert.strictEqual(lineTokens.getCount(), 7); - assert.equal(lineTokens.getStartOffset(0), 0); - assert.equal(lineTokens.getEndOffset(0), 6); - assert.equal(lineTokens.getStartOffset(1), 6); - assert.equal(lineTokens.getEndOffset(1), 13); - assert.equal(lineTokens.getStartOffset(2), 13); - assert.equal(lineTokens.getEndOffset(2), 18); - assert.equal(lineTokens.getStartOffset(3), 18); - assert.equal(lineTokens.getEndOffset(3), 21); - assert.equal(lineTokens.getStartOffset(4), 21); - assert.equal(lineTokens.getEndOffset(4), 23); - assert.equal(lineTokens.getStartOffset(5), 23); - assert.equal(lineTokens.getEndOffset(5), 30); - assert.equal(lineTokens.getStartOffset(6), 30); - assert.equal(lineTokens.getEndOffset(6), 33); + assert.strictEqual(lineTokens.getStartOffset(0), 0); + assert.strictEqual(lineTokens.getEndOffset(0), 6); + assert.strictEqual(lineTokens.getStartOffset(1), 6); + assert.strictEqual(lineTokens.getEndOffset(1), 13); + assert.strictEqual(lineTokens.getStartOffset(2), 13); + assert.strictEqual(lineTokens.getEndOffset(2), 18); + assert.strictEqual(lineTokens.getStartOffset(3), 18); + assert.strictEqual(lineTokens.getEndOffset(3), 21); + assert.strictEqual(lineTokens.getStartOffset(4), 21); + assert.strictEqual(lineTokens.getEndOffset(4), 23); + assert.strictEqual(lineTokens.getStartOffset(5), 23); + assert.strictEqual(lineTokens.getEndOffset(5), 30); + assert.strictEqual(lineTokens.getStartOffset(6), 30); + assert.strictEqual(lineTokens.getEndOffset(6), 33); }); test('findToken', () => { const lineTokens = createTestLineTokens(); - assert.equal(lineTokens.findTokenIndexAtOffset(0), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(1), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(2), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(3), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(4), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(5), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(6), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(7), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(8), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(9), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(10), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(11), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(12), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(13), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(14), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(15), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(16), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(17), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(18), 3); - assert.equal(lineTokens.findTokenIndexAtOffset(19), 3); - assert.equal(lineTokens.findTokenIndexAtOffset(20), 3); - assert.equal(lineTokens.findTokenIndexAtOffset(21), 4); - assert.equal(lineTokens.findTokenIndexAtOffset(22), 4); - assert.equal(lineTokens.findTokenIndexAtOffset(23), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(24), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(25), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(26), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(27), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(28), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(29), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(30), 6); - assert.equal(lineTokens.findTokenIndexAtOffset(31), 6); - assert.equal(lineTokens.findTokenIndexAtOffset(32), 6); - assert.equal(lineTokens.findTokenIndexAtOffset(33), 6); - assert.equal(lineTokens.findTokenIndexAtOffset(34), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(0), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(1), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(2), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(3), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(4), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(5), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(6), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(7), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(8), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(9), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(10), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(11), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(12), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(13), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(14), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(15), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(16), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(17), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(18), 3); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(19), 3); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(20), 3); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(21), 4); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(22), 4); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(23), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(24), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(25), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(26), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(27), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(28), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(29), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(30), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(31), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(32), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(33), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(34), 6); }); interface ITestViewLineToken { @@ -118,7 +118,7 @@ suite('LineTokens', () => { foreground: _actual.getForeground(i) }; } - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('inflate', () => { diff --git a/src/vs/editor/test/common/core/range.test.ts b/src/vs/editor/test/common/core/range.test.ts index 5420ae452..26415e8c6 100644 --- a/src/vs/editor/test/common/core/range.test.ts +++ b/src/vs/editor/test/common/core/range.test.ts @@ -9,47 +9,47 @@ import { Range } from 'vs/editor/common/core/range'; suite('Editor Core - Range', () => { test('empty range', () => { let s = new Range(1, 1, 1, 1); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 1); - assert.equal(s.endLineNumber, 1); - assert.equal(s.endColumn, 1); - assert.equal(s.isEmpty(), true); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 1); + assert.strictEqual(s.endLineNumber, 1); + assert.strictEqual(s.endColumn, 1); + assert.strictEqual(s.isEmpty(), true); }); test('swap start and stop same line', () => { let s = new Range(1, 2, 1, 1); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 1); - assert.equal(s.endLineNumber, 1); - assert.equal(s.endColumn, 2); - assert.equal(s.isEmpty(), false); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 1); + assert.strictEqual(s.endLineNumber, 1); + assert.strictEqual(s.endColumn, 2); + assert.strictEqual(s.isEmpty(), false); }); test('swap start and stop', () => { let s = new Range(2, 1, 1, 2); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 2); - assert.equal(s.endLineNumber, 2); - assert.equal(s.endColumn, 1); - assert.equal(s.isEmpty(), false); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 2); + assert.strictEqual(s.endLineNumber, 2); + assert.strictEqual(s.endColumn, 1); + assert.strictEqual(s.isEmpty(), false); }); test('no swap same line', () => { let s = new Range(1, 1, 1, 2); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 1); - assert.equal(s.endLineNumber, 1); - assert.equal(s.endColumn, 2); - assert.equal(s.isEmpty(), false); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 1); + assert.strictEqual(s.endLineNumber, 1); + assert.strictEqual(s.endColumn, 2); + assert.strictEqual(s.isEmpty(), false); }); test('no swap', () => { let s = new Range(1, 1, 2, 1); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 1); - assert.equal(s.endLineNumber, 2); - assert.equal(s.endColumn, 1); - assert.equal(s.isEmpty(), false); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 1); + assert.strictEqual(s.endLineNumber, 2); + assert.strictEqual(s.endColumn, 1); + assert.strictEqual(s.isEmpty(), false); }); test('compareRangesUsingEnds', () => { @@ -93,36 +93,36 @@ suite('Editor Core - Range', () => { }); test('containsPosition', () => { - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(1, 3)), false); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(2, 1)), false); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(2, 2)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(2, 3)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(3, 1)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(5, 9)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(5, 10)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(5, 11)), false); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(6, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(1, 3)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(2, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(2, 2)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(2, 3)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(3, 1)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(5, 9)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(5, 10)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(5, 11)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(6, 1)), false); }); test('containsRange', () => { - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(1, 3, 2, 2)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 1, 2, 2)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 5, 11)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 6, 1)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(5, 9, 6, 1)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(5, 10, 6, 1)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 5, 10)), true); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 3, 5, 9)), true); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(3, 100, 4, 100)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(1, 3, 2, 2)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 1, 2, 2)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 5, 11)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 6, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(5, 9, 6, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(5, 10, 6, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 5, 10)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 3, 5, 9)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(3, 100, 4, 100)), true); }); test('areIntersecting', () => { - assert.equal(Range.areIntersecting(new Range(2, 2, 3, 2), new Range(4, 2, 5, 2)), false); - assert.equal(Range.areIntersecting(new Range(4, 2, 5, 2), new Range(2, 2, 3, 2)), false); - assert.equal(Range.areIntersecting(new Range(4, 2, 5, 2), new Range(5, 2, 6, 2)), false); - assert.equal(Range.areIntersecting(new Range(5, 2, 6, 2), new Range(4, 2, 5, 2)), false); - assert.equal(Range.areIntersecting(new Range(2, 2, 2, 7), new Range(2, 4, 2, 6)), true); - assert.equal(Range.areIntersecting(new Range(2, 2, 2, 7), new Range(2, 4, 2, 9)), true); - assert.equal(Range.areIntersecting(new Range(2, 4, 2, 9), new Range(2, 2, 2, 7)), true); + assert.strictEqual(Range.areIntersecting(new Range(2, 2, 3, 2), new Range(4, 2, 5, 2)), false); + assert.strictEqual(Range.areIntersecting(new Range(4, 2, 5, 2), new Range(2, 2, 3, 2)), false); + assert.strictEqual(Range.areIntersecting(new Range(4, 2, 5, 2), new Range(5, 2, 6, 2)), false); + assert.strictEqual(Range.areIntersecting(new Range(5, 2, 6, 2), new Range(4, 2, 5, 2)), false); + assert.strictEqual(Range.areIntersecting(new Range(2, 2, 2, 7), new Range(2, 4, 2, 6)), true); + assert.strictEqual(Range.areIntersecting(new Range(2, 2, 2, 7), new Range(2, 4, 2, 9)), true); + assert.strictEqual(Range.areIntersecting(new Range(2, 4, 2, 9), new Range(2, 2, 2, 7)), true); }); }); diff --git a/src/vs/editor/test/common/diff/diffComputer.test.ts b/src/vs/editor/test/common/diff/diffComputer.test.ts index 9361e05d6..32dd4bce0 100644 --- a/src/vs/editor/test/common/diff/diffComputer.test.ts +++ b/src/vs/editor/test/common/diff/diffComputer.test.ts @@ -64,7 +64,7 @@ function assertDiff(originalLines: string[], modifiedLines: string[], expectedCh for (let i = 0; i < changes.length; i++) { extracted.push(extractLineChangeRepresentation(changes[i], (i < expectedChanges.length ? expectedChanges[i] : null))); } - assert.deepEqual(extracted, expectedChanges); + assert.deepStrictEqual(extracted, expectedChanges); } function createLineDeletion(startLineNumber: number, endLineNumber: number, modifiedLineNumber: number): ILineChange { @@ -462,6 +462,13 @@ suite('Editor Diff - DiffComputer', () => { assertDiff(original, modified, expected, true, false, true); }); + test('empty diff 5', () => { + let original = ['']; + let modified = ['']; + let expected: ILineChange[] = []; + assertDiff(original, modified, expected, true, false, true); + }); + test('pretty diff 1', () => { let original = [ 'suite(function () {', diff --git a/src/vs/editor/test/common/mocks/testConfiguration.ts b/src/vs/editor/test/common/mocks/testConfiguration.ts index a30fbc237..ec94f2201 100644 --- a/src/vs/editor/test/common/mocks/testConfiguration.ts +++ b/src/vs/editor/test/common/mocks/testConfiguration.ts @@ -30,6 +30,7 @@ export class TestConfiguration extends CommonEditorConfiguration { protected readConfiguration(styling: BareFontInfo): FontInfo { return new FontInfo({ zoomLevel: 0, + pixelRatio: 1, fontFamily: 'mockFont', fontWeight: 'normal', fontSize: 14, diff --git a/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts b/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts index 79d4e1e46..357d3bb55 100644 --- a/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts +++ b/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts @@ -58,7 +58,7 @@ export class BenchmarkSuite { let timeDiffTotal = 0; for (let j = 0; j < this.iterations; j++) { let factory = benchmark.buildBuffer(builder); - let buffer = factory.create(DefaultEndOfLine.LF); + let buffer = factory.create(DefaultEndOfLine.LF).textBuffer; benchmark.preCycle(buffer); let start = process.hrtime(); benchmark.fn(buffer); diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index 67c8a119e..c0e4be805 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -22,10 +22,10 @@ suite('EditorModel - EditableTextModel.applyEdits updates mightContainRTL', () = let model = createEditableTextModelFromString(original.join('\n')); model.setEOL(EndOfLineSequence.LF); - assert.equal(model.mightContainRTL(), before); + assert.strictEqual(model.mightContainRTL(), before); model.applyEdits(edits); - assert.equal(model.mightContainRTL(), after); + assert.strictEqual(model.mightContainRTL(), after); model.dispose(); } @@ -68,10 +68,10 @@ suite('EditorModel - EditableTextModel.applyEdits updates mightContainNonBasicAS let model = createEditableTextModelFromString(original.join('\n')); model.setEOL(EndOfLineSequence.LF); - assert.equal(model.mightContainNonBasicASCII(), before); + assert.strictEqual(model.mightContainNonBasicASCII(), before); model.applyEdits(edits); - assert.equal(model.mightContainNonBasicASCII(), after); + assert.strictEqual(model.mightContainNonBasicASCII(), after); model.dispose(); } @@ -1043,7 +1043,7 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { test('issue #1580: Changes in line endings are not correctly reflected in the extension host, leading to invalid offsets sent to external refactoring tools', () => { let model = createEditableTextModelFromString('Hello\nWorld!'); - assert.equal(model.getEOL(), '\n'); + assert.strictEqual(model.getEOL(), '\n'); let mirrorModel2 = new MirrorTextModel(null!, model.getLinesContent(), model.getEOL(), model.getVersionId()); let mirrorModel2PrevVersionId = model.getVersionId(); @@ -1058,8 +1058,8 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { }); let assertMirrorModels = () => { - assert.equal(mirrorModel2.getText(), model.getValue(), 'mirror model 2 text OK'); - assert.equal(mirrorModel2.version, model.getVersionId(), 'mirror model 2 version OK'); + assert.strictEqual(mirrorModel2.getText(), model.getValue(), 'mirror model 2 text OK'); + assert.strictEqual(mirrorModel2.version, model.getVersionId(), 'mirror model 2 version OK'); }; model.setEOL(EndOfLineSequence.CRLF); @@ -1077,16 +1077,16 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { { range: new Range(1, 2, 1, 2), text: '"' }, ]); - assert.equal(model.getValue(EndOfLinePreference.LF), '"\'"👁\''); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '"\'"👁\''); - assert.deepEqual(model.validateRange(new Range(1, 3, 1, 4)), new Range(1, 3, 1, 4)); + assert.deepStrictEqual(model.validateRange(new Range(1, 3, 1, 4)), new Range(1, 3, 1, 4)); model.applyEdits([ { range: new Range(1, 1, 1, 2), text: null }, { range: new Range(1, 3, 1, 4), text: null }, ]); - assert.equal(model.getValue(EndOfLinePreference.LF), '\'👁\''); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\'👁\''); model.dispose(); }); @@ -1108,7 +1108,7 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { model.applyEdits(undoEdits); - assert.deepEqual(model.getValue(), 'line1\nline2\nline3\n'); + assert.deepStrictEqual(model.getValue(), 'line1\nline2\nline3\n'); model.dispose(); }); diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts index be89c0160..58e534c09 100644 --- a/src/vs/editor/test/common/model/intervalTree.test.ts +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -111,7 +111,7 @@ suite('IntervalTree', () => { let actualNodes = this._tree.intervalSearch(op.begin, op.end, 0, false, 0); let actual = actualNodes.map(n => new Interval(n.cachedAbsoluteStart, n.cachedAbsoluteEnd)); let expected = this._oracle.search(new Interval(op.begin, op.end)); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); return; } @@ -123,7 +123,7 @@ suite('IntervalTree', () => { let actual = this._tree.getAllInOrder().map(n => new Interval(n.cachedAbsoluteStart, n.cachedAbsoluteEnd)); let expected = this._oracle.intervals; - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } public getExistingNodeId(index: number): number { @@ -500,7 +500,7 @@ suite('IntervalTree', () => { function assertIntervalSearch(start: number, end: number, expected: [number, number][]): void { let actualNodes = T.intervalSearch(start, end, 0, false, 0); let actual = actualNodes.map((n) => <[number, number]>[n.cachedAbsoluteStart, n.cachedAbsoluteEnd]); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('cormen 1->2', () => { @@ -559,7 +559,7 @@ suite('IntervalTree', () => { let node = new IntervalNode('', nodeStart, nodeEnd); setNodeStickiness(node, nodeStickiness); nodeAcceptEdit(node, start, end, textLength, forceMoveMarkers); - assert.deepEqual([node.start, node.end], [expectedNodeStart, expectedNodeEnd], msg); + assert.deepStrictEqual([node.start, node.end], [expectedNodeStart, expectedNodeEnd], msg); } test('nodeAcceptEdit', () => { diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts index 4c9bd0d18..f385fdda9 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts @@ -33,7 +33,7 @@ suite('PieceTreeTextBuffer._getInverseEdits', () => { function assertInverseEdits(ops: IValidatedEditOperation[], expected: Range[]): void { let actual = PieceTreeTextBuffer._getInverseEditRanges(ops); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('single insert', () => { @@ -282,10 +282,10 @@ suite('PieceTreeTextBuffer._toSingleEditOperation', () => { } function testToSingleEditOperation(original: string[], edits: IValidatedEditOperation[], expected: IValidatedEditOperation): void { - const textBuffer = createTextBufferFactory(original.join('\n')).create(DefaultEndOfLine.LF); + const textBuffer = createTextBufferFactory(original.join('\n')).create(DefaultEndOfLine.LF).textBuffer; const actual = textBuffer._toSingleEditOperation(edits); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('one edit op is unchanged', () => { diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts index dafa53c07..83b4a90d9 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts @@ -10,11 +10,11 @@ import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/ import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; export function testTextBufferFactory(text: string, eol: string, mightContainNonBasicASCII: boolean, mightContainRTL: boolean): void { - const textBuffer = createTextBufferFactory(text).create(DefaultEndOfLine.LF); + const textBuffer = createTextBufferFactory(text).create(DefaultEndOfLine.LF).textBuffer; - assert.equal(textBuffer.mightContainNonBasicASCII(), mightContainNonBasicASCII); - assert.equal(textBuffer.mightContainRTL(), mightContainRTL); - assert.equal(textBuffer.getEOL(), eol); + assert.strictEqual(textBuffer.mightContainNonBasicASCII(), mightContainNonBasicASCII); + assert.strictEqual(textBuffer.mightContainRTL(), mightContainRTL); + assert.strictEqual(textBuffer.getEOL(), eol); } suite('ModelBuilder', () => { diff --git a/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts b/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts index 4f87bf638..069b5553b 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts @@ -6,7 +6,7 @@ import { CharCode } from 'vs/base/common/charCode'; import { splitLines } from 'vs/base/common/strings'; import { Range } from 'vs/editor/common/core/range'; -import { DefaultEndOfLine, ITextBuffer, ITextBufferBuilder, ValidAnnotatedEditOperation } from 'vs/editor/common/model'; +import { ValidAnnotatedEditOperation } from 'vs/editor/common/model'; export function getRandomInt(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; @@ -127,25 +127,6 @@ export function generateRandomReplaces(chunks: string[], editCnt: number, search return ops; } -export function createMockText(lineCount: number, minColumn: number, maxColumn: number) { - let fixedEOL = getRandomEOLSequence(); - let lines: string[] = []; - for (let i = 0; i < lineCount; i++) { - if (i !== 0) { - lines.push(fixedEOL); - } - lines.push(getRandomString(minColumn, maxColumn)); - } - return lines.join(''); -} - -export function createMockBuffer(str: string, bufferBuilder: ITextBufferBuilder): ITextBuffer { - bufferBuilder.acceptChunk(str); - let bufferFactory = bufferBuilder.finish(); - let buffer = bufferFactory.create(DefaultEndOfLine.LF); - return buffer; -} - export function generateRandomChunkWithLF(minLength: number, maxLength: number): string { let length = getRandomInt(minLength, maxLength); let r = ''; diff --git a/src/vs/editor/test/common/model/model.line.test.ts b/src/vs/editor/test/common/model/model.line.test.ts index 139b169bf..b07f24810 100644 --- a/src/vs/editor/test/common/model/model.line.test.ts +++ b/src/vs/editor/test/common/model/model.line.test.ts @@ -39,13 +39,13 @@ function assertLineTokens(__actual: LineTokens, _expected: TestToken[]): void { type: token.getType() }; }; - assert.deepEqual(actual, expected.map(decode)); + assert.deepStrictEqual(actual, expected.map(decode)); } suite('ModelLine - getIndentLevel', () => { function assertIndentLevel(text: string, expected: number, tabSize: number = 4): void { let actual = TextModel.computeIndentLevel(text, tabSize); - assert.equal(actual, expected, text); + assert.strictEqual(actual, expected, text); } test('getIndentLevel', () => { @@ -126,7 +126,7 @@ suite('ModelLinesTokens', () => { for (let lineIndex = 0; lineIndex < expected.length; lineIndex++) { const actualLine = model.getLineContent(lineIndex + 1); const actualTokens = model.getLineTokens(lineIndex + 1); - assert.equal(actualLine, expected[lineIndex].text); + assert.strictEqual(actualLine, expected[lineIndex].text); assertLineTokens(actualTokens, expected[lineIndex].tokens); } } diff --git a/src/vs/editor/test/common/model/model.modes.test.ts b/src/vs/editor/test/common/model/model.modes.test.ts index ce313b77a..1d255659e 100644 --- a/src/vs/editor/test/common/model/model.modes.test.ts +++ b/src/vs/editor/test/common/model/model.modes.test.ts @@ -21,14 +21,14 @@ suite('Editor Model - Model Modes 1', () => { let calledFor: string[] = []; function checkAndClear(arr: string[]) { - assert.deepEqual(calledFor, arr); + assert.deepStrictEqual(calledFor, arr); calledFor = []; } const tokenizationSupport: modes.ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: modes.IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 => { calledFor.push(line.charAt(0)); return new TokenizationResult2(new Uint32Array(0), state); } @@ -106,7 +106,7 @@ suite('Editor Model - Model Modes 1', () => { checkAndClear(['1', '2', '3', '4', '5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '0\n-\n+')]); - assert.equal(thisModel.getLineCount(), 7); + assert.strictEqual(thisModel.getLineCount(), 7); thisModel.forceTokenization(7); checkAndClear(['0', '-', '+']); @@ -174,14 +174,14 @@ suite('Editor Model - Model Modes 2', () => { let calledFor: string[] = []; function checkAndClear(arr: string[]): void { - assert.deepEqual(calledFor, arr); + assert.deepStrictEqual(calledFor, arr); calledFor = []; } const tokenizationSupport: modes.ITokenizationSupport = { getInitialState: () => new ModelState2(''), tokenize: undefined!, - tokenize2: (line: string, state: modes.IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 => { calledFor.push(line); (state).prevLineContent = line; return new TokenizationResult2(new Uint32Array(0), state); diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index ed6787bc5..bfdcfc946 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -46,50 +46,50 @@ suite('Editor Model - Model', () => { // --------- insert text test('model getValue', () => { - assert.equal(thisModel.getValue(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1'); + assert.strictEqual(thisModel.getValue(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1'); }); test('model insert empty text', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '')]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'My First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'My First Line'); }); test('model insert text without newline 1', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'foo My First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'foo My First Line'); }); test('model insert text without newline 2', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' foo')]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'My foo First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'My foo First Line'); }); test('model insert text with one newline', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]); - assert.equal(thisModel.getLineCount(), 6); - assert.equal(thisModel.getLineContent(1), 'My new line'); - assert.equal(thisModel.getLineContent(2), 'No longer First Line'); + assert.strictEqual(thisModel.getLineCount(), 6); + assert.strictEqual(thisModel.getLineContent(1), 'My new line'); + assert.strictEqual(thisModel.getLineContent(2), 'No longer First Line'); }); test('model insert text with two newlines', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nOne more line in the middle\nNo longer')]); - assert.equal(thisModel.getLineCount(), 7); - assert.equal(thisModel.getLineContent(1), 'My new line'); - assert.equal(thisModel.getLineContent(2), 'One more line in the middle'); - assert.equal(thisModel.getLineContent(3), 'No longer First Line'); + assert.strictEqual(thisModel.getLineCount(), 7); + assert.strictEqual(thisModel.getLineContent(1), 'My new line'); + assert.strictEqual(thisModel.getLineContent(2), 'One more line in the middle'); + assert.strictEqual(thisModel.getLineContent(3), 'No longer First Line'); }); test('model insert text with many newlines', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 3), '\n\n\n\n')]); - assert.equal(thisModel.getLineCount(), 9); - assert.equal(thisModel.getLineContent(1), 'My'); - assert.equal(thisModel.getLineContent(2), ''); - assert.equal(thisModel.getLineContent(3), ''); - assert.equal(thisModel.getLineContent(4), ''); - assert.equal(thisModel.getLineContent(5), ' First Line'); + assert.strictEqual(thisModel.getLineCount(), 9); + assert.strictEqual(thisModel.getLineContent(1), 'My'); + assert.strictEqual(thisModel.getLineContent(2), ''); + assert.strictEqual(thisModel.getLineContent(3), ''); + assert.strictEqual(thisModel.getLineContent(4), ''); + assert.strictEqual(thisModel.getLineContent(5), ' First Line'); }); @@ -111,7 +111,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'foo My First Line') ], @@ -130,7 +130,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'My new line'), new ModelRawLinesInserted(2, 2, ['No longer First Line']), @@ -146,47 +146,47 @@ suite('Editor Model - Model', () => { test('model delete empty text', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 1))]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'My First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'My First Line'); }); test('model delete text from one line', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'y First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'y First Line'); }); test('model delete text from one line 2', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'a')]); - assert.equal(thisModel.getLineContent(1), 'aMy First Line'); + assert.strictEqual(thisModel.getLineContent(1), 'aMy First Line'); thisModel.applyEdits([EditOperation.delete(new Range(1, 2, 1, 4))]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'a First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'a First Line'); }); test('model delete all text from a line', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), ''); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), ''); }); test('model delete text from two lines', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]); - assert.equal(thisModel.getLineCount(), 4); - assert.equal(thisModel.getLineContent(1), 'My Second Line'); + assert.strictEqual(thisModel.getLineCount(), 4); + assert.strictEqual(thisModel.getLineContent(1), 'My Second Line'); }); test('model delete text from many lines', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]); - assert.equal(thisModel.getLineCount(), 3); - assert.equal(thisModel.getLineContent(1), 'My Third Line'); + assert.strictEqual(thisModel.getLineCount(), 3); + assert.strictEqual(thisModel.getLineContent(1), 'My Third Line'); }); test('model delete everything', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 5, 2))]); - assert.equal(thisModel.getLineCount(), 1); - assert.equal(thisModel.getLineContent(1), ''); + assert.strictEqual(thisModel.getLineCount(), 1); + assert.strictEqual(thisModel.getLineContent(1), ''); }); // --------- delete text eventing @@ -207,7 +207,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'y First Line'), ], @@ -226,7 +226,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, ''), ], @@ -245,7 +245,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'My Second Line'), new ModelRawLinesDeleted(2, 2), @@ -265,7 +265,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'My Third Line'), new ModelRawLinesDeleted(2, 3), @@ -279,31 +279,31 @@ suite('Editor Model - Model', () => { // --------- getValueInRange test('getValueInRange', () => { - assert.equal(thisModel.getValueInRange(new Range(1, 1, 1, 1)), ''); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 1, 2)), 'M'); - assert.equal(thisModel.getValueInRange(new Range(1, 2, 1, 3)), 'y'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 1, 14)), 'My First Line'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 2, 1)), 'My First Line\n'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 1)), ''); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 2)), 'M'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 2, 1, 3)), 'y'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 14)), 'My First Line'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 1)), 'My First Line\n'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'); }); // --------- getValueLengthInRange test('getValueLengthInRange', () => { - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'.length); }); // --------- setValue @@ -316,7 +316,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.setValue('new value'); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawFlush() ], @@ -332,8 +332,8 @@ suite('Editor Model - Model', () => { { range: new Range(1, 1, 1, 1), text: 'b' }, ], true); - assert.deepEqual(res[0].range, new Range(2, 1, 2, 2)); - assert.deepEqual(res[1].range, new Range(1, 1, 1, 2)); + assert.deepStrictEqual(res[0].range, new Range(2, 1, 2, 2)); + assert.deepStrictEqual(res[1].range, new Range(1, 1, 1, 2)); }); }); @@ -358,17 +358,17 @@ suite('Editor Model - Model Line Separators', () => { }); test('model getValue', () => { - assert.equal(thisModel.getValue(), 'My First Line\u2028\t\tMy Second Line\n Third Line\u2028\n1'); + assert.strictEqual(thisModel.getValue(), 'My First Line\u2028\t\tMy Second Line\n Third Line\u2028\n1'); }); test('model lines', () => { - assert.equal(thisModel.getLineCount(), 3); + assert.strictEqual(thisModel.getLineCount(), 3); }); test('Bug 13333:Model should line break on lonely CR too', () => { let model = createTextModel('Hello\rWorld!\r\nAnother line'); - assert.equal(model.getLineCount(), 3); - assert.equal(model.getValue(), 'Hello\r\nWorld!\r\nAnother line'); + assert.strictEqual(model.getLineCount(), 3); + assert.strictEqual(model.getValue(), 'Hello\r\nWorld!\r\nAnother line'); model.dispose(); }); }); @@ -389,7 +389,7 @@ suite('Editor Model - Words', () => { this._register(TokenizationRegistry.register(this.getLanguageIdentifier().language, { getInitialState: (): IState => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { const tokensArr: number[] = []; let prevLanguageId: LanguageIdentifier | undefined = undefined; for (let i = 0; i < line.length; i++) { @@ -434,17 +434,17 @@ suite('Editor Model - Words', () => { const thisModel = createTextModel(text.join('\n')); disposables.push(thisModel); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: 'text', startColumn: 6, endColumn: 10 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 19)), { word: 'some', startColumn: 15, endColumn: 19 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 20)), null); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 21)), { word: 'words', startColumn: 21, endColumn: 26 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 26)), { word: 'words', startColumn: 21, endColumn: 26 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 27)), null); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 28)), null); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: 'text', startColumn: 6, endColumn: 10 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 19)), { word: 'some', startColumn: 15, endColumn: 19 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 20)), null); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 21)), { word: 'words', startColumn: 21, endColumn: 26 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 26)), { word: 'words', startColumn: 21, endColumn: 26 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 27)), null); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 28)), null); }); test('getWordAtPosition at embedded language boundaries', () => { @@ -455,13 +455,13 @@ suite('Editor Model - Words', () => { const model = createTextModel('abab', undefined, outerMode.getLanguageIdentifier()); disposables.push(model); - assert.deepEqual(model.getWordAtPosition(new Position(1, 1)), { word: 'ab', startColumn: 1, endColumn: 3 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 2)), { word: 'ab', startColumn: 1, endColumn: 3 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 3)), { word: 'ab', startColumn: 1, endColumn: 3 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 4)), { word: 'xx', startColumn: 4, endColumn: 6 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 5)), { word: 'xx', startColumn: 4, endColumn: 6 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 6)), { word: 'xx', startColumn: 4, endColumn: 6 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 7)), { word: 'ab', startColumn: 7, endColumn: 9 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 1)), { word: 'ab', startColumn: 1, endColumn: 3 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 2)), { word: 'ab', startColumn: 1, endColumn: 3 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 3)), { word: 'ab', startColumn: 1, endColumn: 3 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 4)), { word: 'xx', startColumn: 4, endColumn: 6 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 5)), { word: 'xx', startColumn: 4, endColumn: 6 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 6)), { word: 'xx', startColumn: 4, endColumn: 6 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 7)), { word: 'ab', startColumn: 7, endColumn: 9 }); }); test('issue #61296: VS code freezes when editing CSS file with emoji', () => { @@ -480,13 +480,13 @@ suite('Editor Model - Words', () => { const thisModel = createTextModel('.🐷-a-b', undefined, MODE_ID); disposables.push(thisModel); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: '.', startColumn: 1, endColumn: 2 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: '.', startColumn: 1, endColumn: 2 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 3)), null); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 7)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 8)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: '.', startColumn: 1, endColumn: 2 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: '.', startColumn: 1, endColumn: 2 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 3)), null); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 7)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 8)), { word: '-a-b', startColumn: 4, endColumn: 8 }); }); }); diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index 4f7690f11..da7b00939 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -28,7 +28,7 @@ function modelHasDecorations(model: TextModel, decorations: ILightWeightDecorati }); } modelDecorations.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - assert.deepEqual(modelDecorations, decorations); + assert.deepStrictEqual(modelDecorations, decorations); } function modelHasDecoration(model: TextModel, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, className: string) { @@ -39,7 +39,7 @@ function modelHasDecoration(model: TextModel, startLineNumber: number, startColu } function modelHasNoDecorations(model: TextModel) { - assert.equal(model.getAllDecorations().length, 0, 'Model has no decoration'); + assert.strictEqual(model.getAllDecorations().length, 0, 'Model has no decoration'); } function addDecoration(model: TextModel, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, className: string): string { @@ -60,7 +60,7 @@ function lineHasDecorations(model: TextModel, lineNumber: number, decorations: { className: decs[i].options.className }); } - assert.deepEqual(lineDecorations, decorations, 'Line decorations'); + assert.deepStrictEqual(lineDecorations, decorations, 'Line decorations'); } function lineHasNoDecorations(model: TextModel, lineNumber: number) { @@ -122,12 +122,12 @@ suite('Editor Model - Model Decorations', () => { addDecoration(thisModel, 1, 1, 2, 1, 'myType'); let line1Decorations = thisModel.getLineDecorations(1); - assert.equal(line1Decorations.length, 1); - assert.equal(line1Decorations[0].options.className, 'myType'); + assert.strictEqual(line1Decorations.length, 1); + assert.strictEqual(line1Decorations[0].options.className, 'myType'); let line2Decorations = thisModel.getLineDecorations(1); - assert.equal(line2Decorations.length, 1); - assert.equal(line2Decorations[0].options.className, 'myType'); + assert.strictEqual(line2Decorations.length, 1); + assert.strictEqual(line2Decorations[0].options.className, 'myType'); lineHasNoDecorations(thisModel, 3); lineHasNoDecorations(thisModel, 4); @@ -138,16 +138,16 @@ suite('Editor Model - Model Decorations', () => { addDecoration(thisModel, 1, 2, 3, 2, 'myType'); let line1Decorations = thisModel.getLineDecorations(1); - assert.equal(line1Decorations.length, 1); - assert.equal(line1Decorations[0].options.className, 'myType'); + assert.strictEqual(line1Decorations.length, 1); + assert.strictEqual(line1Decorations[0].options.className, 'myType'); let line2Decorations = thisModel.getLineDecorations(1); - assert.equal(line2Decorations.length, 1); - assert.equal(line2Decorations[0].options.className, 'myType'); + assert.strictEqual(line2Decorations.length, 1); + assert.strictEqual(line2Decorations[0].options.className, 'myType'); let line3Decorations = thisModel.getLineDecorations(1); - assert.equal(line3Decorations.length, 1); - assert.equal(line3Decorations[0].options.className, 'myType'); + assert.strictEqual(line3Decorations.length, 1); + assert.strictEqual(line3Decorations[0].options.className, 'myType'); lineHasNoDecorations(thisModel, 4); lineHasNoDecorations(thisModel, 5); @@ -209,7 +209,7 @@ suite('Editor Model - Model Decorations', () => { listenerCalled++; }); addDecoration(thisModel, 1, 2, 3, 2, 'myType'); - assert.equal(listenerCalled, 1, 'listener called'); + assert.strictEqual(listenerCalled, 1, 'listener called'); }); test('decorations emit event on change', () => { @@ -221,7 +221,7 @@ suite('Editor Model - Model Decorations', () => { thisModel.changeDecorations((changeAccessor) => { changeAccessor.changeDecoration(decId, new Range(1, 1, 1, 2)); }); - assert.equal(listenerCalled, 1, 'listener called'); + assert.strictEqual(listenerCalled, 1, 'listener called'); }); test('decorations emit event on remove', () => { @@ -233,7 +233,7 @@ suite('Editor Model - Model Decorations', () => { thisModel.changeDecorations((changeAccessor) => { changeAccessor.removeDecoration(decId); }); - assert.equal(listenerCalled, 1, 'listener called'); + assert.strictEqual(listenerCalled, 1, 'listener called'); }); test('decorations emit event when inserting one line text before it', () => { @@ -245,7 +245,7 @@ suite('Editor Model - Model Decorations', () => { }); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'Hallo ')]); - assert.equal(listenerCalled, 1, 'listener called'); + assert.strictEqual(listenerCalled, 1, 'listener called'); }); test('decorations do not emit event on no-op deltaDecorations', () => { @@ -260,7 +260,7 @@ suite('Editor Model - Model Decorations', () => { accessor.deltaDecorations([], []); }); - assert.equal(listenerCalled, 0, 'listener not called'); + assert.strictEqual(listenerCalled, 0, 'listener not called'); }); // --------- editing text & effects on decorations @@ -429,7 +429,7 @@ suite('Decorations and editing', () => { forceMoveMarkers: editForceMoveMarkers }]); const actual = model.getDecorationRange(id); - assert.deepEqual(actual, expectedDecRange, msg); + assert.deepStrictEqual(actual, expectedDecRange, msg); model.dispose(); } @@ -1155,20 +1155,20 @@ suite('deltaDecorations', () => { let initialIds = model.deltaDecorations([], decorations.map(toModelDeltaDecoration)); let actualDecorations = readModelDecorations(model, initialIds); - assert.equal(initialIds.length, decorations.length, 'returns expected cnt of ids'); - assert.equal(initialIds.length, model.getAllDecorations().length, 'does not leak decorations'); + assert.strictEqual(initialIds.length, decorations.length, 'returns expected cnt of ids'); + assert.strictEqual(initialIds.length, model.getAllDecorations().length, 'does not leak decorations'); actualDecorations.sort((a, b) => strcmp(a.id, b.id)); decorations.sort((a, b) => strcmp(a.id, b.id)); - assert.deepEqual(actualDecorations, decorations); + assert.deepStrictEqual(actualDecorations, decorations); let newIds = model.deltaDecorations(initialIds, newDecorations.map(toModelDeltaDecoration)); let actualNewDecorations = readModelDecorations(model, newIds); - assert.equal(newIds.length, newDecorations.length, 'returns expected cnt of ids'); - assert.equal(newIds.length, model.getAllDecorations().length, 'does not leak decorations'); + assert.strictEqual(newIds.length, newDecorations.length, 'returns expected cnt of ids'); + assert.strictEqual(newIds.length, model.getAllDecorations().length, 'does not leak decorations'); actualNewDecorations.sort((a, b) => strcmp(a.id, b.id)); newDecorations.sort((a, b) => strcmp(a.id, b.id)); - assert.deepEqual(actualDecorations, decorations); + assert.deepStrictEqual(actualDecorations, decorations); model.dispose(); } @@ -1188,8 +1188,8 @@ suite('deltaDecorations', () => { toModelDeltaDecoration(decoration('b', 2, 1, 2, 13)) ]); - assert.deepEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); - assert.deepEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); + assert.deepStrictEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); + assert.deepStrictEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); model.dispose(); }); @@ -1294,7 +1294,7 @@ suite('deltaDecorations', () => { let actualDecoration = model.getDecorationOptions(ids[0]); - assert.deepEqual(actualDecoration!.hoverMessage, { value: 'hello2' }); + assert.deepStrictEqual(actualDecoration!.hoverMessage, { value: 'hello2' }); model.dispose(); }); @@ -1326,16 +1326,16 @@ suite('deltaDecorations', () => { toModelDeltaDecoration(decoration('b', 2, 1, 2, 13)) ]); - assert.deepEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); - assert.deepEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); + assert.deepStrictEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); + assert.deepStrictEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); ids = model.deltaDecorations(ids, [ toModelDeltaDecoration(decoration('a', 1, 1, 1, 12)), toModelDeltaDecoration(decoration('b', 2, 1, 2, 13)) ]); - assert.deepEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); - assert.deepEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); + assert.deepStrictEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); + assert.deepStrictEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); model.dispose(); }); @@ -1365,7 +1365,7 @@ suite('deltaDecorations', () => { let inRangeClassNames = inRange.map(d => d.options.className); inRangeClassNames.sort(); - assert.deepEqual(inRangeClassNames, ['x1', 'x2', 'x3', 'x4']); + assert.deepStrictEqual(inRangeClassNames, ['x1', 'x2', 'x3', 'x4']); model.dispose(); }); @@ -1383,7 +1383,7 @@ suite('deltaDecorations', () => { forceMoveMarkers: false }]); const actual = model.getDecorationRange(id); - assert.deepEqual(actual, new Range(1, 1, 1, 1)); + assert.deepStrictEqual(actual, new Range(1, 1, 1, 1)); model.dispose(); }); diff --git a/src/vs/editor/test/common/model/modelEditOperation.test.ts b/src/vs/editor/test/common/model/modelEditOperation.test.ts index 058c5d3d5..d6c2af9b9 100644 --- a/src/vs/editor/test/common/model/modelEditOperation.test.ts +++ b/src/vs/editor/test/common/model/modelEditOperation.test.ts @@ -52,19 +52,19 @@ suite('Editor Model - Model Edit Operation', () => { let inverseEditOp = model.applyEdits(editOp, true); - assert.equal(model.getLineCount(), editedLines.length); + assert.strictEqual(model.getLineCount(), editedLines.length); for (let i = 0; i < editedLines.length; i++) { - assert.equal(model.getLineContent(i + 1), editedLines[i]); + assert.strictEqual(model.getLineContent(i + 1), editedLines[i]); } let originalOp = model.applyEdits(inverseEditOp, true); - assert.equal(model.getLineCount(), 5); - assert.equal(model.getLineContent(1), LINE1); - assert.equal(model.getLineContent(2), LINE2); - assert.equal(model.getLineContent(3), LINE3); - assert.equal(model.getLineContent(4), LINE4); - assert.equal(model.getLineContent(5), LINE5); + assert.strictEqual(model.getLineCount(), 5); + assert.strictEqual(model.getLineContent(1), LINE1); + assert.strictEqual(model.getLineContent(2), LINE2); + assert.strictEqual(model.getLineContent(3), LINE3); + assert.strictEqual(model.getLineContent(4), LINE4); + assert.strictEqual(model.getLineContent(5), LINE5); const simplifyEdit = (edit: IIdentifiedSingleEditOperation) => { return { @@ -75,7 +75,7 @@ suite('Editor Model - Model Edit Operation', () => { isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit || false }; }; - assert.deepEqual(originalOp.map(simplifyEdit), editOp.map(simplifyEdit)); + assert.deepStrictEqual(originalOp.map(simplifyEdit), editOp.map(simplifyEdit)); } test('Insert inline', () => { diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index 019b23f4f..04fc6d743 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -77,11 +77,11 @@ function trimLineFeed(text: string): string { function testLinesContent(str: string, pieceTable: PieceTreeBase) { let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLinesRawContent(), str); for (let i = 0; i < lines.length; i++) { - assert.equal(pieceTable.getLineContent(i + 1), lines[i]); - assert.equal( + assert.strictEqual(pieceTable.getLineContent(i + 1), lines[i]); + assert.strictEqual( trimLineFeed( pieceTable.getValueInRange( new Range( @@ -136,16 +136,16 @@ function testLineStarts(str: string, pieceTable: PieceTreeBase) { } while (m); for (let i = 0; i < lineStarts.length; i++) { - assert.deepEqual( + assert.deepStrictEqual( pieceTable.getPositionAt(lineStarts[i]), new Position(i + 1, 1) ); - assert.equal(pieceTable.getOffsetAt(i + 1, 1), lineStarts[i]); + assert.strictEqual(pieceTable.getOffsetAt(i + 1, 1), lineStarts[i]); } for (let i = 1; i < lineStarts.length; i++) { let pos = pieceTable.getPositionAt(lineStarts[i] - 1); - assert.equal( + assert.strictEqual( pieceTable.getOffsetAt(pos.lineNumber, pos.column), lineStarts[i] - 1 ); @@ -158,7 +158,7 @@ function createTextBuffer(val: string[], normalizeEOL: boolean = true): PieceTre bufferBuilder.acceptChunk(chunk); } let factory = bufferBuilder.finish(normalizeEOL); - return (factory.create(DefaultEndOfLine.LF)).getPieceTree(); + return (factory.create(DefaultEndOfLine.LF).textBuffer).getPieceTree(); } function assertTreeInvariants(T: PieceTreeBase): void { @@ -219,12 +219,12 @@ suite('inserts and deletes', () => { ]); pieceTable.insert(34, 'This is some more text to insert at offset 34.'); - assert.equal( + assert.strictEqual( pieceTable.getLinesRawContent(), 'This is a document with some text.This is some more text to insert at offset 34.' ); pieceTable.delete(42, 5); - assert.equal( + assert.strictEqual( pieceTable.getLinesRawContent(), 'This is a document with some text.This is more text to insert at offset 34.' ); @@ -235,28 +235,28 @@ suite('inserts and deletes', () => { let pt = createTextBuffer(['']); pt.insert(0, 'AAA'); - assert.equal(pt.getLinesRawContent(), 'AAA'); + assert.strictEqual(pt.getLinesRawContent(), 'AAA'); pt.insert(0, 'BBB'); - assert.equal(pt.getLinesRawContent(), 'BBBAAA'); + assert.strictEqual(pt.getLinesRawContent(), 'BBBAAA'); pt.insert(6, 'CCC'); - assert.equal(pt.getLinesRawContent(), 'BBBAAACCC'); + assert.strictEqual(pt.getLinesRawContent(), 'BBBAAACCC'); pt.insert(5, 'DDD'); - assert.equal(pt.getLinesRawContent(), 'BBBAADDDACCC'); + assert.strictEqual(pt.getLinesRawContent(), 'BBBAADDDACCC'); assertTreeInvariants(pt); }); test('more deletes', () => { let pt = createTextBuffer(['012345678']); pt.delete(8, 1); - assert.equal(pt.getLinesRawContent(), '01234567'); + assert.strictEqual(pt.getLinesRawContent(), '01234567'); pt.delete(0, 1); - assert.equal(pt.getLinesRawContent(), '1234567'); + assert.strictEqual(pt.getLinesRawContent(), '1234567'); pt.delete(5, 1); - assert.equal(pt.getLinesRawContent(), '123457'); + assert.strictEqual(pt.getLinesRawContent(), '123457'); pt.delete(5, 1); - assert.equal(pt.getLinesRawContent(), '12345'); + assert.strictEqual(pt.getLinesRawContent(), '12345'); pt.delete(0, 5); - assert.equal(pt.getLinesRawContent(), ''); + assert.strictEqual(pt.getLinesRawContent(), ''); assertTreeInvariants(pt); }); @@ -265,17 +265,17 @@ suite('inserts and deletes', () => { let pieceTable = createTextBuffer(['']); pieceTable.insert(0, 'ceLPHmFzvCtFeHkCBej '); str = str.substring(0, 0) + 'ceLPHmFzvCtFeHkCBej ' + str.substring(0); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(8, 'gDCEfNYiBUNkSwtvB K '); str = str.substring(0, 8) + 'gDCEfNYiBUNkSwtvB K ' + str.substring(8); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(38, 'cyNcHxjNPPoehBJldLS '); str = str.substring(0, 38) + 'cyNcHxjNPPoehBJldLS ' + str.substring(38); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(59, 'ejMx\nOTgWlbpeDExjOk '); str = str.substring(0, 59) + 'ejMx\nOTgWlbpeDExjOk ' + str.substring(59); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -293,7 +293,7 @@ suite('inserts and deletes', () => { pieceTable.insert(10, 'Gbtp '); str = str.substring(0, 10) + 'Gbtp ' + str.substring(10); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -310,7 +310,7 @@ suite('inserts and deletes', () => { str = str.substring(0, 2) + 'GGZB' + str.substring(2); pieceTable.insert(12, 'wXpq'); str = str.substring(0, 12) + 'wXpq' + str.substring(12); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); }); test('random delete 1', () => { @@ -319,30 +319,30 @@ suite('inserts and deletes', () => { pieceTable.insert(0, 'vfb'); str = str.substring(0, 0) + 'vfb' + str.substring(0); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(0, 'zRq'); str = str.substring(0, 0) + 'zRq' + str.substring(0); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(5, 1); str = str.substring(0, 5) + str.substring(5 + 1); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(1, 'UNw'); str = str.substring(0, 1) + 'UNw' + str.substring(1); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(4, 3); str = str.substring(0, 4) + str.substring(4 + 3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(1, 4); str = str.substring(0, 1) + str.substring(1 + 4); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(0, 1); str = str.substring(0, 0) + str.substring(0 + 1); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -368,7 +368,7 @@ suite('inserts and deletes', () => { str = str.substring(0, 6) + str.substring(6 + 7); pieceTable.delete(3, 5); str = str.substring(0, 3) + str.substring(3 + 5); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -401,7 +401,7 @@ suite('inserts and deletes', () => { str = str.substring(0, 5) + str.substring(5 + 8); pieceTable.delete(3, 4); str = str.substring(0, 3) + str.substring(3 + 4); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -427,7 +427,7 @@ suite('inserts and deletes', () => { pieceTable.insert(5, '\n\na\r'); str = str.substring(0, 5) + '\n\na\r' + str.substring(5); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -455,7 +455,7 @@ suite('inserts and deletes', () => { pieceTable.insert(2, 'a\ra\n'); str = str.substring(0, 2) + 'a\ra\n' + str.substring(2); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -480,11 +480,11 @@ suite('inserts and deletes', () => { str = str.substring(0, 5) + '\n\na\r' + str.substring(5); pieceTable.insert(10, '\r\r\n\r'); str = str.substring(0, 10) + '\r\r\n\r' + str.substring(10); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(21, 3); str = str.substring(0, 21) + str.substring(21 + 3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -512,7 +512,7 @@ suite('inserts and deletes', () => { pieceTable.insert(3, 'a\naa'); str = str.substring(0, 3) + 'a\naa' + str.substring(3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); test('random insert/delete \\r bug 5', () => { @@ -539,7 +539,7 @@ suite('inserts and deletes', () => { pieceTable.insert(15, '\n\r\r\r'); str = str.substring(0, 15) + '\n\r\r\r' + str.substring(15); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); }); @@ -548,22 +548,22 @@ suite('prefix sum for line feed', () => { test('basic', () => { let pieceTable = createTextBuffer(['1\n2\n3\n4']); - assert.equal(pieceTable.getLineCount(), 4); - assert.deepEqual(pieceTable.getPositionAt(0), new Position(1, 1)); - assert.deepEqual(pieceTable.getPositionAt(1), new Position(1, 2)); - assert.deepEqual(pieceTable.getPositionAt(2), new Position(2, 1)); - assert.deepEqual(pieceTable.getPositionAt(3), new Position(2, 2)); - assert.deepEqual(pieceTable.getPositionAt(4), new Position(3, 1)); - assert.deepEqual(pieceTable.getPositionAt(5), new Position(3, 2)); - assert.deepEqual(pieceTable.getPositionAt(6), new Position(4, 1)); + assert.strictEqual(pieceTable.getLineCount(), 4); + assert.deepStrictEqual(pieceTable.getPositionAt(0), new Position(1, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(1), new Position(1, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(2), new Position(2, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(3), new Position(2, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(4), new Position(3, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(5), new Position(3, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(6), new Position(4, 1)); - assert.equal(pieceTable.getOffsetAt(1, 1), 0); - assert.equal(pieceTable.getOffsetAt(1, 2), 1); - assert.equal(pieceTable.getOffsetAt(2, 1), 2); - assert.equal(pieceTable.getOffsetAt(2, 2), 3); - assert.equal(pieceTable.getOffsetAt(3, 1), 4); - assert.equal(pieceTable.getOffsetAt(3, 2), 5); - assert.equal(pieceTable.getOffsetAt(4, 1), 6); + assert.strictEqual(pieceTable.getOffsetAt(1, 1), 0); + assert.strictEqual(pieceTable.getOffsetAt(1, 2), 1); + assert.strictEqual(pieceTable.getOffsetAt(2, 1), 2); + assert.strictEqual(pieceTable.getOffsetAt(2, 2), 3); + assert.strictEqual(pieceTable.getOffsetAt(3, 1), 4); + assert.strictEqual(pieceTable.getOffsetAt(3, 2), 5); + assert.strictEqual(pieceTable.getOffsetAt(4, 1), 6); assertTreeInvariants(pieceTable); }); @@ -571,9 +571,9 @@ suite('prefix sum for line feed', () => { let pieceTable = createTextBuffer(['a\nb\nc\nde']); pieceTable.insert(8, 'fh\ni\njk'); - assert.equal(pieceTable.getLineCount(), 6); - assert.deepEqual(pieceTable.getPositionAt(9), new Position(4, 4)); - assert.equal(pieceTable.getOffsetAt(1, 1), 0); + assert.strictEqual(pieceTable.getLineCount(), 6); + assert.deepStrictEqual(pieceTable.getPositionAt(9), new Position(4, 4)); + assert.strictEqual(pieceTable.getOffsetAt(1, 1), 0); assertTreeInvariants(pieceTable); }); @@ -581,22 +581,22 @@ suite('prefix sum for line feed', () => { let pieceTable = createTextBuffer(['a\nb\nc\nde']); pieceTable.insert(7, 'fh\ni\njk'); - assert.equal(pieceTable.getLineCount(), 6); - assert.deepEqual(pieceTable.getPositionAt(6), new Position(4, 1)); - assert.deepEqual(pieceTable.getPositionAt(7), new Position(4, 2)); - assert.deepEqual(pieceTable.getPositionAt(8), new Position(4, 3)); - assert.deepEqual(pieceTable.getPositionAt(9), new Position(4, 4)); - assert.deepEqual(pieceTable.getPositionAt(12), new Position(6, 1)); - assert.deepEqual(pieceTable.getPositionAt(13), new Position(6, 2)); - assert.deepEqual(pieceTable.getPositionAt(14), new Position(6, 3)); + assert.strictEqual(pieceTable.getLineCount(), 6); + assert.deepStrictEqual(pieceTable.getPositionAt(6), new Position(4, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(7), new Position(4, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(8), new Position(4, 3)); + assert.deepStrictEqual(pieceTable.getPositionAt(9), new Position(4, 4)); + assert.deepStrictEqual(pieceTable.getPositionAt(12), new Position(6, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(13), new Position(6, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(14), new Position(6, 3)); - assert.equal(pieceTable.getOffsetAt(4, 1), 6); - assert.equal(pieceTable.getOffsetAt(4, 2), 7); - assert.equal(pieceTable.getOffsetAt(4, 3), 8); - assert.equal(pieceTable.getOffsetAt(4, 4), 9); - assert.equal(pieceTable.getOffsetAt(6, 1), 12); - assert.equal(pieceTable.getOffsetAt(6, 2), 13); - assert.equal(pieceTable.getOffsetAt(6, 3), 14); + assert.strictEqual(pieceTable.getOffsetAt(4, 1), 6); + assert.strictEqual(pieceTable.getOffsetAt(4, 2), 7); + assert.strictEqual(pieceTable.getOffsetAt(4, 3), 8); + assert.strictEqual(pieceTable.getOffsetAt(4, 4), 9); + assert.strictEqual(pieceTable.getOffsetAt(6, 1), 12); + assert.strictEqual(pieceTable.getOffsetAt(6, 2), 13); + assert.strictEqual(pieceTable.getOffsetAt(6, 3), 14); assertTreeInvariants(pieceTable); }); @@ -604,23 +604,23 @@ suite('prefix sum for line feed', () => { let pieceTable = createTextBuffer(['a\nb\nc\ndefh\ni\njk']); pieceTable.delete(7, 2); - assert.equal(pieceTable.getLinesRawContent(), 'a\nb\nc\ndh\ni\njk'); - assert.equal(pieceTable.getLineCount(), 6); - assert.deepEqual(pieceTable.getPositionAt(6), new Position(4, 1)); - assert.deepEqual(pieceTable.getPositionAt(7), new Position(4, 2)); - assert.deepEqual(pieceTable.getPositionAt(8), new Position(4, 3)); - assert.deepEqual(pieceTable.getPositionAt(9), new Position(5, 1)); - assert.deepEqual(pieceTable.getPositionAt(11), new Position(6, 1)); - assert.deepEqual(pieceTable.getPositionAt(12), new Position(6, 2)); - assert.deepEqual(pieceTable.getPositionAt(13), new Position(6, 3)); + assert.strictEqual(pieceTable.getLinesRawContent(), 'a\nb\nc\ndh\ni\njk'); + assert.strictEqual(pieceTable.getLineCount(), 6); + assert.deepStrictEqual(pieceTable.getPositionAt(6), new Position(4, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(7), new Position(4, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(8), new Position(4, 3)); + assert.deepStrictEqual(pieceTable.getPositionAt(9), new Position(5, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(11), new Position(6, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(12), new Position(6, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(13), new Position(6, 3)); - assert.equal(pieceTable.getOffsetAt(4, 1), 6); - assert.equal(pieceTable.getOffsetAt(4, 2), 7); - assert.equal(pieceTable.getOffsetAt(4, 3), 8); - assert.equal(pieceTable.getOffsetAt(5, 1), 9); - assert.equal(pieceTable.getOffsetAt(6, 1), 11); - assert.equal(pieceTable.getOffsetAt(6, 2), 12); - assert.equal(pieceTable.getOffsetAt(6, 3), 13); + assert.strictEqual(pieceTable.getOffsetAt(4, 1), 6); + assert.strictEqual(pieceTable.getOffsetAt(4, 2), 7); + assert.strictEqual(pieceTable.getOffsetAt(4, 3), 8); + assert.strictEqual(pieceTable.getOffsetAt(5, 1), 9); + assert.strictEqual(pieceTable.getOffsetAt(6, 1), 11); + assert.strictEqual(pieceTable.getOffsetAt(6, 2), 12); + assert.strictEqual(pieceTable.getOffsetAt(6, 3), 13); assertTreeInvariants(pieceTable); }); @@ -629,23 +629,23 @@ suite('prefix sum for line feed', () => { pieceTable.insert(8, 'fh\ni\njk'); pieceTable.delete(7, 2); - assert.equal(pieceTable.getLinesRawContent(), 'a\nb\nc\ndh\ni\njk'); - assert.equal(pieceTable.getLineCount(), 6); - assert.deepEqual(pieceTable.getPositionAt(6), new Position(4, 1)); - assert.deepEqual(pieceTable.getPositionAt(7), new Position(4, 2)); - assert.deepEqual(pieceTable.getPositionAt(8), new Position(4, 3)); - assert.deepEqual(pieceTable.getPositionAt(9), new Position(5, 1)); - assert.deepEqual(pieceTable.getPositionAt(11), new Position(6, 1)); - assert.deepEqual(pieceTable.getPositionAt(12), new Position(6, 2)); - assert.deepEqual(pieceTable.getPositionAt(13), new Position(6, 3)); + assert.strictEqual(pieceTable.getLinesRawContent(), 'a\nb\nc\ndh\ni\njk'); + assert.strictEqual(pieceTable.getLineCount(), 6); + assert.deepStrictEqual(pieceTable.getPositionAt(6), new Position(4, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(7), new Position(4, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(8), new Position(4, 3)); + assert.deepStrictEqual(pieceTable.getPositionAt(9), new Position(5, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(11), new Position(6, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(12), new Position(6, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(13), new Position(6, 3)); - assert.equal(pieceTable.getOffsetAt(4, 1), 6); - assert.equal(pieceTable.getOffsetAt(4, 2), 7); - assert.equal(pieceTable.getOffsetAt(4, 3), 8); - assert.equal(pieceTable.getOffsetAt(5, 1), 9); - assert.equal(pieceTable.getOffsetAt(6, 1), 11); - assert.equal(pieceTable.getOffsetAt(6, 2), 12); - assert.equal(pieceTable.getOffsetAt(6, 3), 13); + assert.strictEqual(pieceTable.getOffsetAt(4, 1), 6); + assert.strictEqual(pieceTable.getOffsetAt(4, 2), 7); + assert.strictEqual(pieceTable.getOffsetAt(4, 3), 8); + assert.strictEqual(pieceTable.getOffsetAt(5, 1), 9); + assert.strictEqual(pieceTable.getOffsetAt(6, 1), 11); + assert.strictEqual(pieceTable.getOffsetAt(6, 2), 12); + assert.strictEqual(pieceTable.getOffsetAt(6, 3), 13); assertTreeInvariants(pieceTable); }); @@ -661,7 +661,7 @@ suite('prefix sum for line feed', () => { str = str.substring(0, 14) + 'X ZZ\nYZZYZXXY Y XY\n ' + str.substring(14); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -675,7 +675,7 @@ suite('prefix sum for line feed', () => { pieceTable.insert(3, 'XXY \n\nY Y YYY ZYXY '); str = str.substring(0, 3) + 'XXY \n\nY Y YYY ZYXY ' + str.substring(3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -803,12 +803,12 @@ suite('get text in range', () => { pieceTable.delete(7, 2); // 'a\nb\nc\ndh\ni\njk' - assert.equal(pieceTable.getValueInRange(new Range(1, 1, 1, 3)), 'a\n'); - assert.equal(pieceTable.getValueInRange(new Range(2, 1, 2, 3)), 'b\n'); - assert.equal(pieceTable.getValueInRange(new Range(3, 1, 3, 3)), 'c\n'); - assert.equal(pieceTable.getValueInRange(new Range(4, 1, 4, 4)), 'dh\n'); - assert.equal(pieceTable.getValueInRange(new Range(5, 1, 5, 3)), 'i\n'); - assert.equal(pieceTable.getValueInRange(new Range(6, 1, 6, 3)), 'jk'); + assert.strictEqual(pieceTable.getValueInRange(new Range(1, 1, 1, 3)), 'a\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(2, 1, 2, 3)), 'b\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(3, 1, 3, 3)), 'c\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(4, 1, 4, 4)), 'dh\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(5, 1, 5, 3)), 'i\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(6, 1, 6, 3)), 'jk'); assertTreeInvariants(pieceTable); }); @@ -902,18 +902,18 @@ suite('get text in range', () => { test('get line content', () => { let pieceTable = createTextBuffer(['1']); - assert.equal(pieceTable.getLineRawContent(1), '1'); + assert.strictEqual(pieceTable.getLineRawContent(1), '1'); pieceTable.insert(1, '2'); - assert.equal(pieceTable.getLineRawContent(1), '12'); + assert.strictEqual(pieceTable.getLineRawContent(1), '12'); assertTreeInvariants(pieceTable); }); test('get line content basic', () => { let pieceTable = createTextBuffer(['1\n2\n3\n4']); - assert.equal(pieceTable.getLineRawContent(1), '1\n'); - assert.equal(pieceTable.getLineRawContent(2), '2\n'); - assert.equal(pieceTable.getLineRawContent(3), '3\n'); - assert.equal(pieceTable.getLineRawContent(4), '4'); + assert.strictEqual(pieceTable.getLineRawContent(1), '1\n'); + assert.strictEqual(pieceTable.getLineRawContent(2), '2\n'); + assert.strictEqual(pieceTable.getLineRawContent(3), '3\n'); + assert.strictEqual(pieceTable.getLineRawContent(4), '4'); assertTreeInvariants(pieceTable); }); @@ -923,12 +923,12 @@ suite('get text in range', () => { pieceTable.delete(7, 2); // 'a\nb\nc\ndh\ni\njk' - assert.equal(pieceTable.getLineRawContent(1), 'a\n'); - assert.equal(pieceTable.getLineRawContent(2), 'b\n'); - assert.equal(pieceTable.getLineRawContent(3), 'c\n'); - assert.equal(pieceTable.getLineRawContent(4), 'dh\n'); - assert.equal(pieceTable.getLineRawContent(5), 'i\n'); - assert.equal(pieceTable.getLineRawContent(6), 'jk'); + assert.strictEqual(pieceTable.getLineRawContent(1), 'a\n'); + assert.strictEqual(pieceTable.getLineRawContent(2), 'b\n'); + assert.strictEqual(pieceTable.getLineRawContent(3), 'c\n'); + assert.strictEqual(pieceTable.getLineRawContent(4), 'dh\n'); + assert.strictEqual(pieceTable.getLineRawContent(5), 'i\n'); + assert.strictEqual(pieceTable.getLineRawContent(6), 'jk'); assertTreeInvariants(pieceTable); }); @@ -973,7 +973,7 @@ suite('CRLF', () => { pieceTable.insert(0, 'a\r\nb'); pieceTable.delete(0, 2); - assert.equal(pieceTable.getLineCount(), 2); + assert.strictEqual(pieceTable.getLineCount(), 2); assertTreeInvariants(pieceTable); }); @@ -982,7 +982,7 @@ suite('CRLF', () => { pieceTable.insert(0, 'a\r\nb'); pieceTable.delete(2, 2); - assert.equal(pieceTable.getLineCount(), 2); + assert.strictEqual(pieceTable.getLineCount(), 2); assertTreeInvariants(pieceTable); }); @@ -999,7 +999,7 @@ suite('CRLF', () => { str = str.substring(0, 2) + str.substring(2 + 3); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); test('random bug 2', () => { @@ -1014,7 +1014,7 @@ suite('CRLF', () => { str = str.substring(0, 4) + str.substring(4 + 1); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); test('random bug 3', () => { @@ -1035,7 +1035,7 @@ suite('CRLF', () => { str = str.substring(0, 3) + '\r\r\r\n' + str.substring(3); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); test('random bug 4', () => { @@ -1185,14 +1185,14 @@ suite('centralized lineStarts with CRLF', () => { test('delete CR in CRLF 1', () => { let pieceTable = createTextBuffer(['a\r\nb'], false); pieceTable.delete(2, 2); - assert.equal(pieceTable.getLineCount(), 2); + assert.strictEqual(pieceTable.getLineCount(), 2); assertTreeInvariants(pieceTable); }); test('delete CR in CRLF 2', () => { let pieceTable = createTextBuffer(['a\r\nb']); pieceTable.delete(0, 2); - assert.equal(pieceTable.getLineCount(), 2); + assert.strictEqual(pieceTable.getLineCount(), 2); assertTreeInvariants(pieceTable); }); @@ -1207,7 +1207,7 @@ suite('centralized lineStarts with CRLF', () => { str = str.substring(0, 2) + str.substring(2 + 3); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); test('random bug 2', () => { @@ -1220,7 +1220,7 @@ suite('centralized lineStarts with CRLF', () => { str = str.substring(0, 4) + str.substring(4 + 1); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); @@ -1240,7 +1240,7 @@ suite('centralized lineStarts with CRLF', () => { str = str.substring(0, 3) + '\r\r\r\n' + str.substring(3); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); @@ -1386,7 +1386,7 @@ suite('centralized lineStarts with CRLF', () => { pieceTable.insert(7, '\r\r\r\r'); str = str.substring(0, 7) + '\r\r\r\r' + str.substring(7); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -1407,7 +1407,7 @@ suite('centralized lineStarts with CRLF', () => { pieceTable.delete(11, 2); str = str.substring(0, 11) + str.substring(11 + 2); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -1424,7 +1424,7 @@ suite('centralized lineStarts with CRLF', () => { pieceTable.delete(1, 2); str = str.substring(0, 1) + str.substring(1 + 2); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -1437,7 +1437,7 @@ suite('centralized lineStarts with CRLF', () => { pieceTable.insert(3, '\r\n\n\n'); str = str.substring(0, 3) + '\r\n\n\n' + str.substring(3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -1469,7 +1469,7 @@ suite('random is unsupervised', () => { pieceTable.insert(0, 'VZXXZYZX\r'); str = str.substring(0, 0) + 'VZXXZYZX\r' + str.substring(0); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); testLinesContent(str, pieceTable); @@ -1507,7 +1507,7 @@ suite('random is unsupervised', () => { } // console.log(output); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); testLinesContent(str, pieceTable); @@ -1543,7 +1543,7 @@ suite('random is unsupervised', () => { } } - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); testLinesContent(str, pieceTable); assertTreeInvariants(pieceTable); @@ -1577,7 +1577,7 @@ suite('random is unsupervised', () => { testLinesContent(str, pieceTable); } - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); testLinesContent(str, pieceTable); assertTreeInvariants(pieceTable); @@ -1612,33 +1612,33 @@ suite('buffer api', () => { test('getLineCharCode - issue #45735', () => { let pieceTable = createTextBuffer(['LINE1\nline2']); - assert.equal(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); - assert.equal(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); - assert.equal(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); - assert.equal(pieceTable.getLineCharCode(1, 3), 'E'.charCodeAt(0), 'E'); - assert.equal(pieceTable.getLineCharCode(1, 4), '1'.charCodeAt(0), '1'); - assert.equal(pieceTable.getLineCharCode(1, 5), '\n'.charCodeAt(0), '\\n'); - assert.equal(pieceTable.getLineCharCode(2, 0), 'l'.charCodeAt(0), 'l'); - assert.equal(pieceTable.getLineCharCode(2, 1), 'i'.charCodeAt(0), 'i'); - assert.equal(pieceTable.getLineCharCode(2, 2), 'n'.charCodeAt(0), 'n'); - assert.equal(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); - assert.equal(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); + assert.strictEqual(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); + assert.strictEqual(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); + assert.strictEqual(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); + assert.strictEqual(pieceTable.getLineCharCode(1, 3), 'E'.charCodeAt(0), 'E'); + assert.strictEqual(pieceTable.getLineCharCode(1, 4), '1'.charCodeAt(0), '1'); + assert.strictEqual(pieceTable.getLineCharCode(1, 5), '\n'.charCodeAt(0), '\\n'); + assert.strictEqual(pieceTable.getLineCharCode(2, 0), 'l'.charCodeAt(0), 'l'); + assert.strictEqual(pieceTable.getLineCharCode(2, 1), 'i'.charCodeAt(0), 'i'); + assert.strictEqual(pieceTable.getLineCharCode(2, 2), 'n'.charCodeAt(0), 'n'); + assert.strictEqual(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); + assert.strictEqual(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); }); test('getLineCharCode - issue #47733', () => { let pieceTable = createTextBuffer(['', 'LINE1\n', 'line2']); - assert.equal(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); - assert.equal(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); - assert.equal(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); - assert.equal(pieceTable.getLineCharCode(1, 3), 'E'.charCodeAt(0), 'E'); - assert.equal(pieceTable.getLineCharCode(1, 4), '1'.charCodeAt(0), '1'); - assert.equal(pieceTable.getLineCharCode(1, 5), '\n'.charCodeAt(0), '\\n'); - assert.equal(pieceTable.getLineCharCode(2, 0), 'l'.charCodeAt(0), 'l'); - assert.equal(pieceTable.getLineCharCode(2, 1), 'i'.charCodeAt(0), 'i'); - assert.equal(pieceTable.getLineCharCode(2, 2), 'n'.charCodeAt(0), 'n'); - assert.equal(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); - assert.equal(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); + assert.strictEqual(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); + assert.strictEqual(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); + assert.strictEqual(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); + assert.strictEqual(pieceTable.getLineCharCode(1, 3), 'E'.charCodeAt(0), 'E'); + assert.strictEqual(pieceTable.getLineCharCode(1, 4), '1'.charCodeAt(0), '1'); + assert.strictEqual(pieceTable.getLineCharCode(1, 5), '\n'.charCodeAt(0), '\\n'); + assert.strictEqual(pieceTable.getLineCharCode(2, 0), 'l'.charCodeAt(0), 'l'); + assert.strictEqual(pieceTable.getLineCharCode(2, 1), 'i'.charCodeAt(0), 'i'); + assert.strictEqual(pieceTable.getLineCharCode(2, 2), 'n'.charCodeAt(0), 'n'); + assert.strictEqual(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); + assert.strictEqual(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); }); }); @@ -1771,7 +1771,7 @@ suite('snapshot', () => { ]); const snapshot = model.createSnapshot(); const snapshot1 = model.createSnapshot(); - assert.equal(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); model.applyEdits([ { @@ -1786,7 +1786,7 @@ suite('snapshot', () => { } ]); - assert.equal(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot1)); + assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot1)); }); test('immutable snapshot 1', () => { @@ -1806,7 +1806,7 @@ suite('snapshot', () => { } ]); - assert.equal(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); }); test('immutable snapshot 2', () => { @@ -1826,7 +1826,7 @@ suite('snapshot', () => { } ]); - assert.equal(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); }); test('immutable snapshot 3', () => { @@ -1845,7 +1845,7 @@ suite('snapshot', () => { } ]); - assert.notEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + assert.notStrictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); }); }); @@ -1854,7 +1854,7 @@ suite('chunk based search', () => { let pieceTree = createTextBuffer(['']); pieceTree.delete(0, 1); let ret = pieceTree.findMatchesLineByLine(new Range(1, 1, 1, 1), new SearchData(/abc/, new WordCharacterClassifier(',./'), 'abc'), true, 1000); - assert.equal(ret.length, 0); + assert.strictEqual(ret.length, 0); }); test('#45770. FindInNode should not cross node boundary.', () => { @@ -1873,11 +1873,11 @@ suite('chunk based search', () => { pieceTree.insert(16, ' '); let ret = pieceTree.findMatchesLineByLine(new Range(1, 1, 4, 13), new SearchData(/\[/gi, new WordCharacterClassifier(',./'), '['), true, 1000); - assert.equal(ret.length, 3); + assert.strictEqual(ret.length, 3); - assert.deepEqual(ret[0].range, new Range(2, 3, 2, 4)); - assert.deepEqual(ret[1].range, new Range(3, 3, 3, 4)); - assert.deepEqual(ret[2].range, new Range(4, 3, 4, 4)); + assert.deepStrictEqual(ret[0].range, new Range(2, 3, 2, 4)); + assert.deepStrictEqual(ret[1].range, new Range(3, 3, 3, 4)); + assert.deepStrictEqual(ret[2].range, new Range(4, 3, 4, 4)); }); test('search searching from the middle', () => { @@ -1889,12 +1889,12 @@ suite('chunk based search', () => { ]); pieceTree.delete(4, 1); let ret = pieceTree.findMatchesLineByLine(new Range(2, 3, 2, 6), new SearchData(/a/gi, null, 'a'), true, 1000); - assert.equal(ret.length, 1); - assert.deepEqual(ret[0].range, new Range(2, 3, 2, 4)); + assert.strictEqual(ret.length, 1); + assert.deepStrictEqual(ret[0].range, new Range(2, 3, 2, 4)); pieceTree.delete(4, 1); ret = pieceTree.findMatchesLineByLine(new Range(2, 2, 2, 5), new SearchData(/a/gi, null, 'a'), true, 1000); - assert.equal(ret.length, 1); - assert.deepEqual(ret[0].range, new Range(2, 2, 2, 3)); + assert.strictEqual(ret.length, 1); + assert.deepStrictEqual(ret[0].range, new Range(2, 2, 2, 3)); }); }); diff --git a/src/vs/editor/test/common/model/textChange.test.ts b/src/vs/editor/test/common/model/textChange.test.ts index d6d42dfc6..bbc29ea57 100644 --- a/src/vs/editor/test/common/model/textChange.test.ts +++ b/src/vs/editor/test/common/model/textChange.test.ts @@ -75,7 +75,7 @@ suite('TextChangeCompressor', () => { }; }); let actualDoResult = getResultingContent(initialText, compressedDoTextEdits); - assert.equal(actualDoResult, finalText); + assert.strictEqual(actualDoResult, finalText); let compressedUndoTextEdits: IGeneratedEdit[] = compressedTextChanges.map((change) => { return { @@ -85,7 +85,7 @@ suite('TextChangeCompressor', () => { }; }); let actualUndoResult = getResultingContent(finalText, compressedUndoTextEdits); - assert.equal(actualUndoResult, initialText); + assert.strictEqual(actualUndoResult, initialText); } test('simple 1', () => { diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index 2da334170..2d8e6bbde 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -22,8 +22,8 @@ function testGuessIndentation(defaultInsertSpaces: boolean, defaultTabSize: numb let r = m.getOptions(); m.dispose(); - assert.equal(r.insertSpaces, expectedInsertSpaces, msg); - assert.equal(r.tabSize, expectedTabSize, msg); + assert.strictEqual(r.insertSpaces, expectedInsertSpaces, msg); + assert.strictEqual(r.tabSize, expectedTabSize, msg); } function assertGuess(expectedInsertSpaces: boolean | undefined, expectedTabSize: number | undefined | [number], text: string[], msg?: string): void { @@ -75,14 +75,14 @@ suite('TextModelData.fromString', () => { } function testTextModelDataFromString(text: string, expected: ITextBufferData): void { - const textBuffer = createTextBuffer(text, TextModel.DEFAULT_CREATION_OPTIONS.defaultEOL); + const textBuffer = createTextBuffer(text, TextModel.DEFAULT_CREATION_OPTIONS.defaultEOL).textBuffer; let actual: ITextBufferData = { EOL: textBuffer.getEOL(), lines: textBuffer.getLinesContent(), containsRTL: textBuffer.mightContainRTL(), isBasicASCII: !textBuffer.mightContainNonBasicASCII() }; - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('one line text', () => { @@ -164,30 +164,30 @@ suite('Editor Model - TextModel', () => { test('getValueLengthInRange', () => { let m = createTextModel('My First Line\r\nMy Second Line\r\nMy Third Line'); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\r\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 1)), 'y First Line\r\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 2)), 'y First Line\r\nM'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 1000)), 'y First Line\r\nMy Second Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 3, 1)), 'y First Line\r\nMy Second Line\r\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\r\nMy Second Line\r\nMy Third Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\r\nMy Second Line\r\nMy Third Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\r\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 1)), 'y First Line\r\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 2)), 'y First Line\r\nM'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 1000)), 'y First Line\r\nMy Second Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1)), 'y First Line\r\nMy Second Line\r\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\r\nMy Second Line\r\nMy Third Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\r\nMy Second Line\r\nMy Third Line'.length); m = createTextModel('My First Line\nMy Second Line\nMy Third Line'); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 1)), 'y First Line\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 2)), 'y First Line\nM'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 1000)), 'y First Line\nMy Second Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 3, 1)), 'y First Line\nMy Second Line\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\nMy Second Line\nMy Third Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\nMy Second Line\nMy Third Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 1)), 'y First Line\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 2)), 'y First Line\nM'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 1000)), 'y First Line\nMy Second Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1)), 'y First Line\nMy Second Line\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\nMy Second Line\nMy Third Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\nMy Second Line\nMy Third Line'.length); }); test('guess indentation 1', () => { @@ -664,69 +664,69 @@ suite('Editor Model - TextModel', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.validatePosition(new Position(0, 0)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(0, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 0)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 30)), new Position(1, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 30)), new Position(1, 9)); - assert.deepEqual(m.validatePosition(new Position(2, 0)), new Position(2, 1)); - assert.deepEqual(m.validatePosition(new Position(2, 1)), new Position(2, 1)); - assert.deepEqual(m.validatePosition(new Position(2, 2)), new Position(2, 2)); - assert.deepEqual(m.validatePosition(new Position(2, 30)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 0)), new Position(2, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 1)), new Position(2, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 2)), new Position(2, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 30)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(3, 0)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(3, 1)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(3, 30)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(3, 0)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(3, 1)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(3, 30)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(30, 30)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(30, 30)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(-123.123, -0.5)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(Number.MIN_VALUE, Number.MIN_VALUE)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(-123.123, -0.5)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(Number.MIN_VALUE, Number.MIN_VALUE)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(Number.MAX_VALUE, Number.MAX_VALUE)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(123.23, 47.5)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(Number.MAX_VALUE, Number.MAX_VALUE)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(123.23, 47.5)), new Position(2, 9)); }); test('validatePosition around high-low surrogate pairs 1', () => { let m = createTextModel('a📚b'); - assert.deepEqual(m.validatePosition(new Position(0, 0)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(0, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(0, 7)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 0)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 7)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 3)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 4)), new Position(1, 4)); - assert.deepEqual(m.validatePosition(new Position(1, 5)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(1, 30)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 3)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 4)), new Position(1, 4)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 5)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 30)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(2, 0)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(2, 1)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(2, 2)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(2, 30)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 0)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 1)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 2)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 30)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(-123.123, -0.5)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(Number.MIN_VALUE, Number.MIN_VALUE)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(-123.123, -0.5)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(Number.MIN_VALUE, Number.MIN_VALUE)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(Number.MAX_VALUE, Number.MAX_VALUE)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(123.23, 47.5)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(Number.MAX_VALUE, Number.MAX_VALUE)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(123.23, 47.5)), new Position(1, 5)); }); test('validatePosition around high-low surrogate pairs 2', () => { let m = createTextModel('a📚📚b'); - assert.deepEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 3)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 4)), new Position(1, 4)); - assert.deepEqual(m.validatePosition(new Position(1, 5)), new Position(1, 4)); - assert.deepEqual(m.validatePosition(new Position(1, 6)), new Position(1, 6)); - assert.deepEqual(m.validatePosition(new Position(1, 7)), new Position(1, 7)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 3)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 4)), new Position(1, 4)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 5)), new Position(1, 4)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 6)), new Position(1, 6)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 7)), new Position(1, 7)); }); @@ -734,133 +734,133 @@ suite('Editor Model - TextModel', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.validatePosition(new Position(NaN, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, NaN)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(NaN, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(1, NaN)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(NaN, NaN)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(2, NaN)), new Position(2, 1)); - assert.deepEqual(m.validatePosition(new Position(NaN, 3)), new Position(1, 3)); + assert.deepStrictEqual(m.validatePosition(new Position(NaN, NaN)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(2, NaN)), new Position(2, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(NaN, 3)), new Position(1, 3)); }); test('issue #71480: validatePosition handle floats', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.validatePosition(new Position(0.2, 1)), new Position(1, 1), 'a'); - assert.deepEqual(m.validatePosition(new Position(1.2, 1)), new Position(1, 1), 'b'); - assert.deepEqual(m.validatePosition(new Position(1.5, 2)), new Position(1, 2), 'c'); - assert.deepEqual(m.validatePosition(new Position(1.8, 3)), new Position(1, 3), 'd'); - assert.deepEqual(m.validatePosition(new Position(1, 0.3)), new Position(1, 1), 'e'); - assert.deepEqual(m.validatePosition(new Position(2, 0.8)), new Position(2, 1), 'f'); - assert.deepEqual(m.validatePosition(new Position(1, 1.2)), new Position(1, 1), 'g'); - assert.deepEqual(m.validatePosition(new Position(2, 1.5)), new Position(2, 1), 'h'); + assert.deepStrictEqual(m.validatePosition(new Position(0.2, 1)), new Position(1, 1), 'a'); + assert.deepStrictEqual(m.validatePosition(new Position(1.2, 1)), new Position(1, 1), 'b'); + assert.deepStrictEqual(m.validatePosition(new Position(1.5, 2)), new Position(1, 2), 'c'); + assert.deepStrictEqual(m.validatePosition(new Position(1.8, 3)), new Position(1, 3), 'd'); + assert.deepStrictEqual(m.validatePosition(new Position(1, 0.3)), new Position(1, 1), 'e'); + assert.deepStrictEqual(m.validatePosition(new Position(2, 0.8)), new Position(2, 1), 'f'); + assert.deepStrictEqual(m.validatePosition(new Position(1, 1.2)), new Position(1, 1), 'g'); + assert.deepStrictEqual(m.validatePosition(new Position(2, 1.5)), new Position(2, 1), 'h'); }); test('issue #71480: validateRange handle floats', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.validateRange(new Range(0.2, 1.5, 0.8, 2.5)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1.2, 1.7, 1.8, 2.2)), new Range(1, 1, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(0.2, 1.5, 0.8, 2.5)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(1.2, 1.7, 1.8, 2.2)), new Range(1, 1, 1, 2)); }); test('validateRange around high-low surrogate pairs 1', () => { let m = createTextModel('a📚b'); - assert.deepEqual(m.validateRange(new Range(0, 0, 0, 1)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(0, 0, 0, 7)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(0, 0, 0, 1)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(0, 0, 0, 7)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 1)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 2)), new Range(1, 1, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 3)), new Range(1, 1, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 4)), new Range(1, 1, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 5)), new Range(1, 1, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 1)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 2)), new Range(1, 1, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 3)), new Range(1, 1, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 4)), new Range(1, 1, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 5)), new Range(1, 1, 1, 5)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 2)), new Range(1, 2, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 3)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 4)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 5)), new Range(1, 2, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 2)), new Range(1, 2, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 3)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 4)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 5)), new Range(1, 2, 1, 5)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 3)), new Range(1, 2, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 4)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 5)), new Range(1, 2, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 3)), new Range(1, 2, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 4)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 5)), new Range(1, 2, 1, 5)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 4)), new Range(1, 4, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 5)), new Range(1, 4, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 4)), new Range(1, 4, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 5)), new Range(1, 4, 1, 5)); - assert.deepEqual(m.validateRange(new Range(1, 5, 1, 5)), new Range(1, 5, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 5, 1, 5)), new Range(1, 5, 1, 5)); }); test('validateRange around high-low surrogate pairs 2', () => { let m = createTextModel('a📚📚b'); - assert.deepEqual(m.validateRange(new Range(0, 0, 0, 1)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(0, 0, 0, 7)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(0, 0, 0, 1)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(0, 0, 0, 7)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 1)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 2)), new Range(1, 1, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 3)), new Range(1, 1, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 4)), new Range(1, 1, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 5)), new Range(1, 1, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 6)), new Range(1, 1, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 7)), new Range(1, 1, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 1)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 2)), new Range(1, 1, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 3)), new Range(1, 1, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 4)), new Range(1, 1, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 5)), new Range(1, 1, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 6)), new Range(1, 1, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 7)), new Range(1, 1, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 2)), new Range(1, 2, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 3)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 4)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 5)), new Range(1, 2, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 6)), new Range(1, 2, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 7)), new Range(1, 2, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 2)), new Range(1, 2, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 3)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 4)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 5)), new Range(1, 2, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 6)), new Range(1, 2, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 7)), new Range(1, 2, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 3)), new Range(1, 2, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 4)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 5)), new Range(1, 2, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 6)), new Range(1, 2, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 7)), new Range(1, 2, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 3)), new Range(1, 2, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 4)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 5)), new Range(1, 2, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 6)), new Range(1, 2, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 7)), new Range(1, 2, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 4)), new Range(1, 4, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 5)), new Range(1, 4, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 6)), new Range(1, 4, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 7)), new Range(1, 4, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 4)), new Range(1, 4, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 5)), new Range(1, 4, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 6)), new Range(1, 4, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 7)), new Range(1, 4, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 5, 1, 5)), new Range(1, 4, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 5, 1, 6)), new Range(1, 4, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 5, 1, 7)), new Range(1, 4, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 5, 1, 5)), new Range(1, 4, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 5, 1, 6)), new Range(1, 4, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 5, 1, 7)), new Range(1, 4, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 6, 1, 6)), new Range(1, 6, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 6, 1, 7)), new Range(1, 6, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 6, 1, 6)), new Range(1, 6, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 6, 1, 7)), new Range(1, 6, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 7, 1, 7)), new Range(1, 7, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 7, 1, 7)), new Range(1, 7, 1, 7)); }); test('modifyPosition', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.modifyPosition(new Position(1, 1), 0), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(0, 0), 0), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(30, 1), 0), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 1), 0), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(0, 0), 0), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(30, 1), 0), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 1), 17), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 1), 1), new Position(1, 2)); - assert.deepEqual(m.modifyPosition(new Position(1, 1), 3), new Position(1, 4)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), 10), new Position(2, 3)); - assert.deepEqual(m.modifyPosition(new Position(1, 5), 13), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), 16), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 1), 17), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 1), 1), new Position(1, 2)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 1), 3), new Position(1, 4)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), 10), new Position(2, 3)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 5), 13), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), 16), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(2, 9), -17), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), -1), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(1, 4), -3), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(2, 3), -10), new Position(1, 2)); - assert.deepEqual(m.modifyPosition(new Position(2, 9), -13), new Position(1, 5)); - assert.deepEqual(m.modifyPosition(new Position(2, 9), -16), new Position(1, 2)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 9), -17), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), -1), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 4), -3), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 3), -10), new Position(1, 2)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 9), -13), new Position(1, 5)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 9), -16), new Position(1, 2)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), 17), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), 100), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), 17), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), 100), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), -2), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), -100), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(2, 2), -100), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(2, 9), -18), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), -2), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), -100), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 2), -100), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 9), -18), new Position(1, 1)); }); test('normalizeIndentation 1', () => { @@ -870,27 +870,27 @@ suite('Editor Model - TextModel', () => { } ); - assert.equal(model.normalizeIndentation('\t'), '\t'); - assert.equal(model.normalizeIndentation(' '), '\t'); - assert.equal(model.normalizeIndentation(' '), ' '); - assert.equal(model.normalizeIndentation(' '), ' '); - assert.equal(model.normalizeIndentation(' '), ' '); - assert.equal(model.normalizeIndentation(''), ''); - assert.equal(model.normalizeIndentation(' \t '), '\t\t'); - assert.equal(model.normalizeIndentation(' \t '), '\t '); - assert.equal(model.normalizeIndentation(' \t '), '\t '); - assert.equal(model.normalizeIndentation(' \t'), '\t '); + assert.strictEqual(model.normalizeIndentation('\t'), '\t'); + assert.strictEqual(model.normalizeIndentation(' '), '\t'); + assert.strictEqual(model.normalizeIndentation(' '), ' '); + assert.strictEqual(model.normalizeIndentation(' '), ' '); + assert.strictEqual(model.normalizeIndentation(' '), ' '); + assert.strictEqual(model.normalizeIndentation(''), ''); + assert.strictEqual(model.normalizeIndentation(' \t '), '\t\t'); + assert.strictEqual(model.normalizeIndentation(' \t '), '\t '); + assert.strictEqual(model.normalizeIndentation(' \t '), '\t '); + assert.strictEqual(model.normalizeIndentation(' \t'), '\t '); - assert.equal(model.normalizeIndentation('\ta'), '\ta'); - assert.equal(model.normalizeIndentation(' a'), '\ta'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation('a'), 'a'); - assert.equal(model.normalizeIndentation(' \t a'), '\t\ta'); - assert.equal(model.normalizeIndentation(' \t a'), '\t a'); - assert.equal(model.normalizeIndentation(' \t a'), '\t a'); - assert.equal(model.normalizeIndentation(' \ta'), '\t a'); + assert.strictEqual(model.normalizeIndentation('\ta'), '\ta'); + assert.strictEqual(model.normalizeIndentation(' a'), '\ta'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation('a'), 'a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), '\t\ta'); + assert.strictEqual(model.normalizeIndentation(' \t a'), '\t a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), '\t a'); + assert.strictEqual(model.normalizeIndentation(' \ta'), '\t a'); model.dispose(); }); @@ -898,16 +898,16 @@ suite('Editor Model - TextModel', () => { test('normalizeIndentation 2', () => { let model = createTextModel(''); - assert.equal(model.normalizeIndentation('\ta'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation('a'), 'a'); - assert.equal(model.normalizeIndentation(' \t a'), ' a'); - assert.equal(model.normalizeIndentation(' \t a'), ' a'); - assert.equal(model.normalizeIndentation(' \t a'), ' a'); - assert.equal(model.normalizeIndentation(' \ta'), ' a'); + assert.strictEqual(model.normalizeIndentation('\ta'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation('a'), 'a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' \ta'), ' a'); model.dispose(); }); @@ -928,18 +928,18 @@ suite('Editor Model - TextModel', () => { '' ].join('\n')); - assert.equal(model.getLineFirstNonWhitespaceColumn(1), 1, '1'); - assert.equal(model.getLineFirstNonWhitespaceColumn(2), 2, '2'); - assert.equal(model.getLineFirstNonWhitespaceColumn(3), 2, '3'); - assert.equal(model.getLineFirstNonWhitespaceColumn(4), 3, '4'); - assert.equal(model.getLineFirstNonWhitespaceColumn(5), 3, '5'); - assert.equal(model.getLineFirstNonWhitespaceColumn(6), 0, '6'); - assert.equal(model.getLineFirstNonWhitespaceColumn(7), 0, '7'); - assert.equal(model.getLineFirstNonWhitespaceColumn(8), 0, '8'); - assert.equal(model.getLineFirstNonWhitespaceColumn(9), 0, '9'); - assert.equal(model.getLineFirstNonWhitespaceColumn(10), 4, '10'); - assert.equal(model.getLineFirstNonWhitespaceColumn(11), 0, '11'); - assert.equal(model.getLineFirstNonWhitespaceColumn(12), 0, '12'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(1), 1, '1'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(2), 2, '2'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(3), 2, '3'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(4), 3, '4'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(5), 3, '5'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(6), 0, '6'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(7), 0, '7'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(8), 0, '8'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(9), 0, '9'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(10), 4, '10'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(11), 0, '11'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(12), 0, '12'); }); test('getLineLastNonWhitespaceColumn', () => { @@ -958,24 +958,24 @@ suite('Editor Model - TextModel', () => { '' ].join('\n')); - assert.equal(model.getLineLastNonWhitespaceColumn(1), 4, '1'); - assert.equal(model.getLineLastNonWhitespaceColumn(2), 4, '2'); - assert.equal(model.getLineLastNonWhitespaceColumn(3), 4, '3'); - assert.equal(model.getLineLastNonWhitespaceColumn(4), 4, '4'); - assert.equal(model.getLineLastNonWhitespaceColumn(5), 4, '5'); - assert.equal(model.getLineLastNonWhitespaceColumn(6), 0, '6'); - assert.equal(model.getLineLastNonWhitespaceColumn(7), 0, '7'); - assert.equal(model.getLineLastNonWhitespaceColumn(8), 0, '8'); - assert.equal(model.getLineLastNonWhitespaceColumn(9), 0, '9'); - assert.equal(model.getLineLastNonWhitespaceColumn(10), 4, '10'); - assert.equal(model.getLineLastNonWhitespaceColumn(11), 0, '11'); - assert.equal(model.getLineLastNonWhitespaceColumn(12), 0, '12'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(1), 4, '1'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(2), 4, '2'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(3), 4, '3'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(4), 4, '4'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(5), 4, '5'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(6), 0, '6'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(7), 0, '7'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(8), 0, '8'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(9), 0, '9'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(10), 4, '10'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(11), 0, '11'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(12), 0, '12'); }); test('#50471. getValueInRange with invalid range', () => { let m = createTextModel('My First Line\r\nMy Second Line\r\nMy Third Line'); - assert.equal(m.getValueInRange(new Range(1, NaN, 1, 3)), 'My'); - assert.equal(m.getValueInRange(new Range(NaN, NaN, NaN, NaN)), ''); + assert.strictEqual(m.getValueInRange(new Range(1, NaN, 1, 3)), 'My'); + assert.strictEqual(m.getValueInRange(new Range(NaN, NaN, NaN, NaN)), ''); }); }); @@ -983,26 +983,26 @@ suite('TextModel.mightContainRTL', () => { test('nope', () => { let model = createTextModel('hello world!'); - assert.equal(model.mightContainRTL(), false); + assert.strictEqual(model.mightContainRTL(), false); }); test('yes', () => { let model = createTextModel('Hello,\nזוהי עובדה מבוססת שדעתו'); - assert.equal(model.mightContainRTL(), true); + assert.strictEqual(model.mightContainRTL(), true); }); test('setValue resets 1', () => { let model = createTextModel('hello world!'); - assert.equal(model.mightContainRTL(), false); + assert.strictEqual(model.mightContainRTL(), false); model.setValue('Hello,\nזוהי עובדה מבוססת שדעתו'); - assert.equal(model.mightContainRTL(), true); + assert.strictEqual(model.mightContainRTL(), true); }); test('setValue resets 2', () => { let model = createTextModel('Hello,\nهناك حقيقة مثبتة منذ زمن طويل'); - assert.equal(model.mightContainRTL(), true); + assert.strictEqual(model.mightContainRTL(), true); model.setValue('hello world!'); - assert.equal(model.mightContainRTL(), false); + assert.strictEqual(model.mightContainRTL(), false); }); }); @@ -1012,24 +1012,24 @@ suite('TextModel.createSnapshot', () => { test('empty file', () => { let model = createTextModel(''); let snapshot = model.createSnapshot(); - assert.equal(snapshot.read(), null); + assert.strictEqual(snapshot.read(), null); model.dispose(); }); test('file with BOM', () => { let model = createTextModel(UTF8_BOM_CHARACTER + 'Hello'); - assert.equal(model.getLineContent(1), 'Hello'); + assert.strictEqual(model.getLineContent(1), 'Hello'); let snapshot = model.createSnapshot(true); - assert.equal(snapshot.read(), UTF8_BOM_CHARACTER + 'Hello'); - assert.equal(snapshot.read(), null); + assert.strictEqual(snapshot.read(), UTF8_BOM_CHARACTER + 'Hello'); + assert.strictEqual(snapshot.read(), null); model.dispose(); }); test('regular file', () => { let model = createTextModel('My First Line\n\t\tMy Second Line\n Third Line\n\n1'); let snapshot = model.createSnapshot(); - assert.equal(snapshot.read(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1'); - assert.equal(snapshot.read(), null); + assert.strictEqual(snapshot.read(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1'); + assert.strictEqual(snapshot.read(), null); model.dispose(); }); @@ -1054,10 +1054,10 @@ suite('TextModel.createSnapshot', () => { // all good } else { actual += tmp2; - assert.equal(snapshot.read(), null); + assert.strictEqual(snapshot.read(), null); } - assert.equal(actual, text); + assert.strictEqual(actual, text); model.dispose(); }); diff --git a/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts index 715cd3032..558664746 100644 --- a/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -19,31 +19,31 @@ suite('TextModelSearch', () => { const usualWordSeparators = getMapForWordSeparators(USUAL_WORD_SEPARATORS); function assertFindMatch(actual: FindMatch | null, expectedRange: Range, expectedMatches: string[] | null = null): void { - assert.deepEqual(actual, new FindMatch(expectedRange, expectedMatches)); + assert.deepStrictEqual(actual, new FindMatch(expectedRange, expectedMatches)); } function _assertFindMatches(model: TextModel, searchParams: SearchParams, expectedMatches: FindMatch[]): void { let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), false, 1000); - assert.deepEqual(actual, expectedMatches, 'findMatches OK'); + assert.deepStrictEqual(actual, expectedMatches, 'findMatches OK'); // test `findNextMatch` let startPos = new Position(1, 1); let match = TextModelSearch.findNextMatch(model, searchParams, startPos, false); - assert.deepEqual(match, expectedMatches[0], `findNextMatch ${startPos}`); + assert.deepStrictEqual(match, expectedMatches[0], `findNextMatch ${startPos}`); for (const expectedMatch of expectedMatches) { startPos = expectedMatch.range.getStartPosition(); match = TextModelSearch.findNextMatch(model, searchParams, startPos, false); - assert.deepEqual(match, expectedMatch, `findNextMatch ${startPos}`); + assert.deepStrictEqual(match, expectedMatch, `findNextMatch ${startPos}`); } // test `findPrevMatch` startPos = new Position(model.getLineCount(), model.getLineMaxColumn(model.getLineCount())); match = TextModelSearch.findPreviousMatch(model, searchParams, startPos, false); - assert.deepEqual(match, expectedMatches[expectedMatches.length - 1], `findPrevMatch ${startPos}`); + assert.deepStrictEqual(match, expectedMatches[expectedMatches.length - 1], `findPrevMatch ${startPos}`); for (const expectedMatch of expectedMatches) { startPos = expectedMatch.range.getEndPosition(); match = TextModelSearch.findPreviousMatch(model, searchParams, startPos, false); - assert.deepEqual(match, expectedMatch, `findPrevMatch ${startPos}`); + assert.deepStrictEqual(match, expectedMatch, `findPrevMatch ${startPos}`); } } @@ -486,7 +486,7 @@ suite('TextModelSearch', () => { let searchParams = new SearchParams('(l(in)e)', true, false, null); let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 100); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ new FindMatch(new Range(1, 5, 1, 9), ['line', 'line', 'in']), new FindMatch(new Range(1, 10, 1, 14), ['line', 'line', 'in']), new FindMatch(new Range(2, 5, 2, 9), ['line', 'line', 'in']), @@ -501,7 +501,7 @@ suite('TextModelSearch', () => { let searchParams = new SearchParams('(l(in)e)\\n', true, false, null); let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 100); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ new FindMatch(new Range(1, 10, 2, 1), ['line\n', 'line', 'in']), new FindMatch(new Range(2, 5, 3, 1), ['line\n', 'line', 'in']), ]); @@ -556,7 +556,7 @@ suite('TextModelSearch', () => { test('\\n matches \\r\\n', () => { let model = createTextModel('a\r\nb\r\nc\r\nd\r\ne\r\nf\r\ng\r\nh\r\ni'); - assert.equal(model.getEOL(), '\r\n'); + assert.strictEqual(model.getEOL(), '\r\n'); let searchParams = new SearchParams('h\\n', true, false, null); let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), true); @@ -579,12 +579,12 @@ suite('TextModelSearch', () => { test('\\r can never be found', () => { let model = createTextModel('a\r\nb\r\nc\r\nd\r\ne\r\nf\r\ng\r\nh\r\ni'); - assert.equal(model.getEOL(), '\r\n'); + assert.strictEqual(model.getEOL(), '\r\n'); let searchParams = new SearchParams('\\r\\n', true, false, null); let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), true); - assert.equal(actual, null); - assert.deepEqual(TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 1000), []); + assert.strictEqual(actual, null); + assert.deepStrictEqual(TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 1000), []); model.dispose(); }); @@ -596,8 +596,8 @@ suite('TextModelSearch', () => { if (expected === null) { assert.ok(actual === null); } else { - assert.deepEqual(actual!.regex, expected.regex); - assert.deepEqual(actual!.simpleSearch, expected.simpleSearch); + assert.deepStrictEqual(actual!.regex, expected.regex); + assert.deepStrictEqual(actual!.simpleSearch, expected.simpleSearch); if (wordSeparators) { assert.ok(actual!.wordSeparators !== null); } else { @@ -769,7 +769,7 @@ suite('TextModelSearch', () => { let searchParams = new SearchParams('\\d*', true, false, null); let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 100); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ new FindMatch(new Range(1, 1, 1, 3), ['10']), new FindMatch(new Range(1, 3, 1, 3), ['']), new FindMatch(new Range(1, 4, 1, 7), ['243']), diff --git a/src/vs/editor/test/common/model/textModelWithTokens.test.ts b/src/vs/editor/test/common/model/textModelWithTokens.test.ts index efb1cf1de..e4560fd82 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -100,7 +100,7 @@ suite('TextModelWithTokens', () => { column: column }); - assert.deepEqual(toRelaxedFoundBracket(actual), toRelaxedFoundBracket(currentExpectedBracket), 'findPrevBracket of ' + lineNumber + ', ' + column); + assert.deepStrictEqual(toRelaxedFoundBracket(actual), toRelaxedFoundBracket(currentExpectedBracket), 'findPrevBracket of ' + lineNumber + ', ' + column); } } } @@ -126,7 +126,7 @@ suite('TextModelWithTokens', () => { column: column }); - assert.deepEqual(toRelaxedFoundBracket(actual), toRelaxedFoundBracket(currentExpectedBracket), 'findNextBracket of ' + lineNumber + ', ' + column); + assert.deepStrictEqual(toRelaxedFoundBracket(actual), toRelaxedFoundBracket(currentExpectedBracket), 'findNextBracket of ' + lineNumber + ', ' + column); } } } @@ -148,12 +148,12 @@ suite('TextModelWithTokens', () => { function assertIsNotBracket(model: TextModel, lineNumber: number, column: number) { const match = model.matchBracket(new Position(lineNumber, column)); - assert.equal(match, null, 'is not matching brackets at ' + lineNumber + ', ' + column); + assert.strictEqual(match, null, 'is not matching brackets at ' + lineNumber + ', ' + column); } function assertIsBracket(model: TextModel, testPosition: Position, expected: [Range, Range]): void { const actual = model.matchBracket(testPosition); - assert.deepEqual(actual, expected, 'matches brackets at ' + testPosition); + assert.deepStrictEqual(actual, expected, 'matches brackets at ' + testPosition); } suite('TextModelWithTokens - bracket matching', () => { @@ -351,7 +351,7 @@ suite('TextModelWithTokens', () => { const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line, state) => { + tokenize2: (line, hasEOL, state) => { switch (line) { case 'function hello() {': { const tokens = new Uint32Array([ @@ -399,8 +399,8 @@ suite('TextModelWithTokens', () => { model.forceTokenization(2); model.forceTokenization(3); - assert.deepEqual(model.matchBracket(new Position(2, 23)), null); - assert.deepEqual(model.matchBracket(new Position(2, 20)), null); + assert.deepStrictEqual(model.matchBracket(new Position(2, 23)), null); + assert.deepStrictEqual(model.matchBracket(new Position(2, 20)), null); model.dispose(); registration1.dispose(); @@ -434,7 +434,7 @@ suite('TextModelWithTokens regression tests', () => { foreground: token.getForeground() }; }; - assert.deepEqual(actual, expected.map(decode)); + assert.deepStrictEqual(actual, expected.map(decode)); } let _tokenId = 10; @@ -446,7 +446,7 @@ suite('TextModelWithTokens regression tests', () => { const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line, state) => { + tokenize2: (line, hasEOL, state) => { let myId = ++_tokenId; let tokens = new Uint32Array(2); tokens[0] = 0; @@ -512,7 +512,7 @@ suite('TextModelWithTokens regression tests', () => { ].join('\n'), undefined, languageIdentifier); let actual = model.matchBracket(new Position(4, 1)); - assert.deepEqual(actual, [new Range(4, 1, 4, 7), new Range(9, 1, 9, 11)]); + assert.deepStrictEqual(actual, [new Range(4, 1, 4, 7), new Range(9, 1, 9, 11)]); model.dispose(); registration.dispose(); @@ -537,7 +537,7 @@ suite('TextModelWithTokens regression tests', () => { ].join('\n'), undefined, languageIdentifier); let actual = model.matchBracket(new Position(3, 9)); - assert.deepEqual(actual, [new Range(3, 6, 3, 17), new Range(2, 6, 2, 14)]); + assert.deepStrictEqual(actual, [new Range(3, 6, 3, 17), new Range(2, 6, 2, 14)]); model.dispose(); registration.dispose(); @@ -550,7 +550,7 @@ suite('TextModelWithTokens regression tests', () => { const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line, state) => { + tokenize2: (line, hasEOL, state) => { let tokens = new Uint32Array(2); tokens[0] = 0; tokens[1] = ( @@ -565,7 +565,7 @@ suite('TextModelWithTokens regression tests', () => { let model = createTextModel('A model with one line', undefined, outerMode); model.forceTokenization(1); - assert.equal(model.getLanguageIdAtPosition(1, 1), innerMode.id); + assert.strictEqual(model.getLanguageIdAtPosition(1, 1), innerMode.id); model.dispose(); registration.dispose(); @@ -586,7 +586,7 @@ suite('TextModel.getLineIndentGuide', () => { actual[line - 1] = [actualIndents[line - 1], activeIndentGuide.startLineNumber, activeIndentGuide.endLineNumber, activeIndentGuide.indent, model.getLineContent(line)]; } - assert.deepEqual(actual, lines); + assert.deepStrictEqual(actual, lines); model.dispose(); } @@ -764,7 +764,7 @@ suite('TextModel.getLineIndentGuide', () => { ].join('\n')); const actual = model.getActiveIndentGuide(2, 4, 9); - assert.deepEqual(actual, { startLineNumber: 2, endLineNumber: 9, indent: 1 }); + assert.deepStrictEqual(actual, { startLineNumber: 2, endLineNumber: 9, indent: 1 }); model.dispose(); }); diff --git a/src/vs/editor/test/common/model/tokensStore.test.ts b/src/vs/editor/test/common/model/tokensStore.test.ts index ee8b438b4..e883d551e 100644 --- a/src/vs/editor/test/common/model/tokensStore.test.ts +++ b/src/vs/editor/test/common/model/tokensStore.test.ts @@ -104,7 +104,7 @@ suite('TokensStore', () => { model.applyEdits(edits); const actualState = extractState(model); - assert.deepEqual(actualState, rawFinalState); + assert.deepStrictEqual(actualState, rawFinalState); model.dispose(); } @@ -191,7 +191,7 @@ suite('TokensStore', () => { decodedTokens.push(lineTokens.getEndOffset(i), lineTokens.getMetadata(i)); } - assert.deepEqual(decodedTokens, [ + assert.deepStrictEqual(decodedTokens, [ 20, 16793600, 24, 17022976, 25, 16793600, @@ -252,7 +252,7 @@ suite('TokensStore', () => { ]); const lineTokens = store.addSemanticTokens(10, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); - assert.equal(lineTokens.getCount(), 3); + assert.strictEqual(lineTokens.getCount(), 3); }); test('partial tokens 2', () => { @@ -293,7 +293,7 @@ suite('TokensStore', () => { ]); const lineTokens = store.addSemanticTokens(20, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); - assert.equal(lineTokens.getCount(), 3); + assert.strictEqual(lineTokens.getCount(), 3); }); test('partial tokens 3', () => { @@ -320,7 +320,7 @@ suite('TokensStore', () => { ]); const lineTokens = store.addSemanticTokens(5, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); - assert.equal(lineTokens.getCount(), 3); + assert.strictEqual(lineTokens.getCount(), 3); }); test('issue #94133: Semantic colors stick around when using (only) range provider', () => { @@ -337,7 +337,7 @@ suite('TokensStore', () => { store.setPartial(new Range(1, 1, 1, 20), []); const lineTokens = store.addSemanticTokens(1, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); - assert.equal(lineTokens.getCount(), 1); + assert.strictEqual(lineTokens.getCount(), 1); }); test('bug', () => { @@ -385,7 +385,7 @@ suite('TokensStore', () => { ); const lineTokens = store.addSemanticTokens(36451, new LineTokens(new Uint32Array([60, 1]), ` if (flags & ModifierFlags.Ambient) {`)); - assert.equal(lineTokens.getCount(), 7); + assert.strictEqual(lineTokens.getCount(), 7); }); @@ -424,7 +424,7 @@ suite('TokensStore', () => { ]), `const hello = 123;`)); const actual = toArr(lineTokens); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ 5, createTMMetadata(5, FontStyle.Bold, 53), 6, createTMMetadata(1, FontStyle.None, 53), 11, createTMMetadata(1, FontStyle.None, 53), diff --git a/src/vs/editor/test/common/modes/languageConfiguration.test.ts b/src/vs/editor/test/common/modes/languageConfiguration.test.ts index e898568a7..3acba6ddf 100644 --- a/src/vs/editor/test/common/modes/languageConfiguration.test.ts +++ b/src/vs/editor/test/common/modes/languageConfiguration.test.ts @@ -11,81 +11,81 @@ suite('StandardAutoClosingPairConditional', () => { test('Missing notIn', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}' }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('Empty notIn', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: [] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('Invalid notIn', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['bla'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('notIn in strings', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['string'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), false); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), false); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('notIn in comments', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['comment'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), false); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), false); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('notIn in regex', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['regex'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), false); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), false); }); test('notIn in strings nor comments', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['string', 'comment'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), false); - assert.equal(v.isOK(StandardTokenType.String), false); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), false); + assert.strictEqual(v.isOK(StandardTokenType.String), false); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('notIn in strings nor regex', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['string', 'regex'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), false); - assert.equal(v.isOK(StandardTokenType.RegEx), false); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), false); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), false); }); test('notIn in comments nor regex', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['comment', 'regex'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), false); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), false); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), false); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), false); }); test('notIn in strings, comments nor regex', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['string', 'comment', 'regex'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), false); - assert.equal(v.isOK(StandardTokenType.String), false); - assert.equal(v.isOK(StandardTokenType.RegEx), false); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), false); + assert.strictEqual(v.isOK(StandardTokenType.String), false); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), false); }); }); diff --git a/src/vs/editor/test/common/modes/languageSelector.test.ts b/src/vs/editor/test/common/modes/languageSelector.test.ts index 0886c65bb..dc06141f6 100644 --- a/src/vs/editor/test/common/modes/languageSelector.test.ts +++ b/src/vs/editor/test/common/modes/languageSelector.test.ts @@ -15,18 +15,18 @@ suite('LanguageSelector', function () { }; test('score, invalid selector', function () { - assert.equal(score({}, model.uri, model.language, true), 0); - assert.equal(score(undefined!, model.uri, model.language, true), 0); - assert.equal(score(null!, model.uri, model.language, true), 0); - assert.equal(score('', model.uri, model.language, true), 0); + assert.strictEqual(score({}, model.uri, model.language, true), 0); + assert.strictEqual(score(undefined!, model.uri, model.language, true), 0); + assert.strictEqual(score(null!, model.uri, model.language, true), 0); + assert.strictEqual(score('', model.uri, model.language, true), 0); }); test('score, any language', function () { - assert.equal(score({ language: '*' }, model.uri, model.language, true), 5); - assert.equal(score('*', model.uri, model.language, true), 5); + assert.strictEqual(score({ language: '*' }, model.uri, model.language, true), 5); + assert.strictEqual(score('*', model.uri, model.language, true), 5); - assert.equal(score('*', URI.parse('foo:bar'), model.language, true), 5); - assert.equal(score('farboo', URI.parse('foo:bar'), model.language, true), 10); + assert.strictEqual(score('*', URI.parse('foo:bar'), model.language, true), 5); + assert.strictEqual(score('farboo', URI.parse('foo:bar'), model.language, true), 10); }); test('score, default schemes', function () { @@ -34,50 +34,50 @@ suite('LanguageSelector', function () { const uri = URI.parse('git:foo/file.txt'); const language = 'farboo'; - assert.equal(score('*', uri, language, true), 5); - assert.equal(score('farboo', uri, language, true), 10); - assert.equal(score({ language: 'farboo', scheme: '' }, uri, language, true), 10); - assert.equal(score({ language: 'farboo', scheme: 'git' }, uri, language, true), 10); - assert.equal(score({ language: 'farboo', scheme: '*' }, uri, language, true), 10); - assert.equal(score({ language: 'farboo' }, uri, language, true), 10); - assert.equal(score({ language: '*' }, uri, language, true), 5); + assert.strictEqual(score('*', uri, language, true), 5); + assert.strictEqual(score('farboo', uri, language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: '' }, uri, language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: 'git' }, uri, language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: '*' }, uri, language, true), 10); + assert.strictEqual(score({ language: 'farboo' }, uri, language, true), 10); + assert.strictEqual(score({ language: '*' }, uri, language, true), 5); - assert.equal(score({ scheme: '*' }, uri, language, true), 5); - assert.equal(score({ scheme: 'git' }, uri, language, true), 10); + assert.strictEqual(score({ scheme: '*' }, uri, language, true), 5); + assert.strictEqual(score({ scheme: 'git' }, uri, language, true), 10); }); test('score, filter', function () { - assert.equal(score('farboo', model.uri, model.language, true), 10); - assert.equal(score({ language: 'farboo' }, model.uri, model.language, true), 10); - assert.equal(score({ language: 'farboo', scheme: 'file' }, model.uri, model.language, true), 10); - assert.equal(score({ language: 'farboo', scheme: 'http' }, model.uri, model.language, true), 0); + assert.strictEqual(score('farboo', model.uri, model.language, true), 10); + assert.strictEqual(score({ language: 'farboo' }, model.uri, model.language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: 'file' }, model.uri, model.language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: 'http' }, model.uri, model.language, true), 0); - assert.equal(score({ pattern: '**/*.fb' }, model.uri, model.language, true), 10); - assert.equal(score({ pattern: '**/*.fb', scheme: 'file' }, model.uri, model.language, true), 10); - assert.equal(score({ pattern: '**/*.fb' }, URI.parse('foo:bar'), model.language, true), 0); - assert.equal(score({ pattern: '**/*.fb', scheme: 'foo' }, URI.parse('foo:bar'), model.language, true), 0); + assert.strictEqual(score({ pattern: '**/*.fb' }, model.uri, model.language, true), 10); + assert.strictEqual(score({ pattern: '**/*.fb', scheme: 'file' }, model.uri, model.language, true), 10); + assert.strictEqual(score({ pattern: '**/*.fb' }, URI.parse('foo:bar'), model.language, true), 0); + assert.strictEqual(score({ pattern: '**/*.fb', scheme: 'foo' }, URI.parse('foo:bar'), model.language, true), 0); let doc = { uri: URI.parse('git:/my/file.js'), langId: 'javascript' }; - assert.equal(score('javascript', doc.uri, doc.langId, true), 10); // 0; - assert.equal(score({ language: 'javascript', scheme: 'git' }, doc.uri, doc.langId, true), 10); // 10; - assert.equal(score('*', doc.uri, doc.langId, true), 5); // 5 - assert.equal(score('fooLang', doc.uri, doc.langId, true), 0); // 0 - assert.equal(score(['fooLang', '*'], doc.uri, doc.langId, true), 5); // 5 + assert.strictEqual(score('javascript', doc.uri, doc.langId, true), 10); // 0; + assert.strictEqual(score({ language: 'javascript', scheme: 'git' }, doc.uri, doc.langId, true), 10); // 10; + assert.strictEqual(score('*', doc.uri, doc.langId, true), 5); // 5 + assert.strictEqual(score('fooLang', doc.uri, doc.langId, true), 0); // 0 + assert.strictEqual(score(['fooLang', '*'], doc.uri, doc.langId, true), 5); // 5 }); test('score, max(filters)', function () { let match = { language: 'farboo', scheme: 'file' }; let fail = { language: 'farboo', scheme: 'http' }; - assert.equal(score(match, model.uri, model.language, true), 10); - assert.equal(score(fail, model.uri, model.language, true), 0); - assert.equal(score([match, fail], model.uri, model.language, true), 10); - assert.equal(score([fail, fail], model.uri, model.language, true), 0); - assert.equal(score(['farboo', '*'], model.uri, model.language, true), 10); - assert.equal(score(['*', 'farboo'], model.uri, model.language, true), 10); + assert.strictEqual(score(match, model.uri, model.language, true), 10); + assert.strictEqual(score(fail, model.uri, model.language, true), 0); + assert.strictEqual(score([match, fail], model.uri, model.language, true), 10); + assert.strictEqual(score([fail, fail], model.uri, model.language, true), 0); + assert.strictEqual(score(['farboo', '*'], model.uri, model.language, true), 10); + assert.strictEqual(score(['*', 'farboo'], model.uri, model.language, true), 10); }); test('score hasAccessToAllModels', function () { @@ -85,14 +85,14 @@ suite('LanguageSelector', function () { uri: URI.parse('file:/my/file.js'), langId: 'javascript' }; - assert.equal(score('javascript', doc.uri, doc.langId, false), 0); - assert.equal(score({ language: 'javascript', scheme: 'file' }, doc.uri, doc.langId, false), 0); - assert.equal(score('*', doc.uri, doc.langId, false), 0); - assert.equal(score('fooLang', doc.uri, doc.langId, false), 0); - assert.equal(score(['fooLang', '*'], doc.uri, doc.langId, false), 0); + assert.strictEqual(score('javascript', doc.uri, doc.langId, false), 0); + assert.strictEqual(score({ language: 'javascript', scheme: 'file' }, doc.uri, doc.langId, false), 0); + assert.strictEqual(score('*', doc.uri, doc.langId, false), 0); + assert.strictEqual(score('fooLang', doc.uri, doc.langId, false), 0); + assert.strictEqual(score(['fooLang', '*'], doc.uri, doc.langId, false), 0); - assert.equal(score({ language: 'javascript', scheme: 'file', hasAccessToAllModels: true }, doc.uri, doc.langId, false), 10); - assert.equal(score(['fooLang', '*', { language: '*', hasAccessToAllModels: true }], doc.uri, doc.langId, false), 5); + assert.strictEqual(score({ language: 'javascript', scheme: 'file', hasAccessToAllModels: true }, doc.uri, doc.langId, false), 10); + assert.strictEqual(score(['fooLang', '*', { language: '*', hasAccessToAllModels: true }], doc.uri, doc.langId, false), 5); }); test('Document selector match - unexpected result value #60232', function () { @@ -102,7 +102,7 @@ suite('LanguageSelector', function () { pattern: '**/*.interface.json' }; let value = score(selector, URI.parse('file:///C:/Users/zlhe/Desktop/test.interface.json'), 'json', true); - assert.equal(value, 10); + assert.strictEqual(value, 10); }); test('Document selector match - platform paths #99938', function () { @@ -113,6 +113,6 @@ suite('LanguageSelector', function () { } }; let value = score(selector, URI.file('/home/user/Desktop/test.json'), 'json', true); - assert.equal(value, 10); + assert.strictEqual(value, 10); }); }); diff --git a/src/vs/editor/test/common/modes/linkComputer.test.ts b/src/vs/editor/test/common/modes/linkComputer.test.ts index 9cf9ca77c..5bb34c911 100644 --- a/src/vs/editor/test/common/modes/linkComputer.test.ts +++ b/src/vs/editor/test/common/modes/linkComputer.test.ts @@ -49,7 +49,7 @@ function assertLink(text: string, extractedLink: string): void { } let r = myComputeLinks([text]); - assert.deepEqual(r, [{ + assert.deepStrictEqual(r, [{ range: { startLineNumber: 1, startColumn: startColumn, @@ -64,7 +64,7 @@ suite('Editor Modes - Link Computer', () => { test('Null model', () => { let r = computeLinks(null); - assert.deepEqual(r, []); + assert.deepStrictEqual(r, []); }); test('Parsing', () => { diff --git a/src/vs/editor/test/common/modes/supports/characterPair.test.ts b/src/vs/editor/test/common/modes/supports/characterPair.test.ts index e244d6b47..ce1c31ee9 100644 --- a/src/vs/editor/test/common/modes/supports/characterPair.test.ts +++ b/src/vs/editor/test/common/modes/supports/characterPair.test.ts @@ -13,44 +13,44 @@ suite('CharacterPairSupport', () => { test('only autoClosingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ autoClosingPairs: [{ open: 'a', close: 'b' }] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), [{ open: 'a', close: 'b', _standardTokenMask: 0 }]); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), [{ open: 'a', close: 'b', _standardTokenMask: 0 }]); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), [new StandardAutoClosingPairConditional({ open: 'a', close: 'b' })]); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), [new StandardAutoClosingPairConditional({ open: 'a', close: 'b' })]); }); test('only empty autoClosingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ autoClosingPairs: [] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), []); }); test('only brackets', () => { let characaterPairSupport = new CharacterPairSupport({ brackets: [['a', 'b']] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), [{ open: 'a', close: 'b', _standardTokenMask: 0 }]); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), [{ open: 'a', close: 'b', _standardTokenMask: 0 }]); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), [new StandardAutoClosingPairConditional({ open: 'a', close: 'b' })]); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), [new StandardAutoClosingPairConditional({ open: 'a', close: 'b' })]); }); test('only empty brackets', () => { let characaterPairSupport = new CharacterPairSupport({ brackets: [] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), []); }); test('only surroundingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ surroundingPairs: [{ open: 'a', close: 'b' }] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), [{ open: 'a', close: 'b' }]); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), [{ open: 'a', close: 'b' }]); }); test('only empty surroundingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ surroundingPairs: [] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), []); }); test('brackets is ignored when having autoClosingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ autoClosingPairs: [], brackets: [['a', 'b']] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), []); }); function findAutoClosingPair(characterPairSupport: CharacterPairSupport, character: string): StandardAutoClosingPairConditional | undefined { @@ -67,64 +67,64 @@ suite('CharacterPairSupport', () => { test('shouldAutoClosePair in empty line', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [], 'a', 1), false); - assert.equal(testShouldAutoClose(sup, [], '{', 1), true); + assert.strictEqual(testShouldAutoClose(sup, [], 'a', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [], '{', 1), true); }); test('shouldAutoClosePair in not interesting line 1', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.Other }], '{', 3), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.Other }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.Other }], '{', 3), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.Other }], 'a', 3), false); }); test('shouldAutoClosePair in not interesting line 2', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}' }] }); - assert.equal(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.String }], '{', 3), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.String }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.String }], '{', 3), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.String }], 'a', 3), false); }); test('shouldAutoClosePair in interesting line 1', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 1), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 1), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 2), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 2), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 2), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 2), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 4), false); }); test('shouldAutoClosePair in interesting line 2', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 1), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 1), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 2), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 2), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 3), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 5), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 5), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 6), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 6), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 7), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 7), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 1), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 2), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 2), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 3), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 5), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 5), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 6), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 6), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 7), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 7), false); }); test('shouldAutoClosePair in interesting line 3', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 1), true); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 1), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 2), true); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 2), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 5), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 5), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 1), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 2), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 2), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 5), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 5), false); }); }); diff --git a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts index 22b818c6b..ba609bdb4 100644 --- a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts +++ b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts @@ -18,12 +18,12 @@ suite('Editor Modes - Auto Indentation', () => { function testDoesNothing(electricCharacterSupport: BracketElectricCharacterSupport, line: TokenText[], character: string, offset: number): void { let actual = _testOnElectricCharacter(electricCharacterSupport, line, character, offset); - assert.deepEqual(actual, null); + assert.deepStrictEqual(actual, null); } function testMatchBracket(electricCharacterSupport: BracketElectricCharacterSupport, line: TokenText[], character: string, offset: number, matchOpenBracket: string): void { let actual = _testOnElectricCharacter(electricCharacterSupport, line, character, offset); - assert.deepEqual(actual, { matchOpenBracket: matchOpenBracket }); + assert.deepStrictEqual(actual, { matchOpenBracket: matchOpenBracket }); } test('getElectricCharacters uses all sources and dedups', () => { @@ -34,7 +34,7 @@ suite('Editor Modes - Auto Indentation', () => { ]) ); - assert.deepEqual(sup.getElectricCharacters(), ['}', ')']); + assert.deepStrictEqual(sup.getElectricCharacters(), ['}', ')']); }); test('matchOpenBracket', () => { diff --git a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts index 722204807..83d8a6ced 100644 --- a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts +++ b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts @@ -18,7 +18,7 @@ export const javascriptOnEnterRules = [ }, { // e.g. * ...| beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, - oneLineAboveText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/, + previousLineText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/, action: { indentAction: IndentAction.None, appendText: '* ' } }, { // e.g. */| diff --git a/src/vs/editor/test/common/modes/supports/onEnter.test.ts b/src/vs/editor/test/common/modes/supports/onEnter.test.ts index f799c66e7..1af36e9ef 100644 --- a/src/vs/editor/test/common/modes/supports/onEnter.test.ts +++ b/src/vs/editor/test/common/modes/supports/onEnter.test.ts @@ -21,9 +21,9 @@ suite('OnEnter', () => { let testIndentAction = (beforeText: string, afterText: string, expected: IndentAction) => { let actual = support.onEnter(EditorAutoIndentStrategy.Advanced, '', beforeText, afterText); if (expected === IndentAction.None) { - assert.equal(actual, null); + assert.strictEqual(actual, null); } else { - assert.equal(actual!.indentAction, expected); + assert.strictEqual(actual!.indentAction, expected); } }; @@ -51,18 +51,18 @@ suite('OnEnter', () => { let support = new OnEnterSupport({ onEnterRules: javascriptOnEnterRules }); - let testIndentAction = (oneLineAboveText: string, beforeText: string, afterText: string, expectedIndentAction: IndentAction | null, expectedAppendText: string | null, removeText: number = 0) => { - let actual = support.onEnter(EditorAutoIndentStrategy.Advanced, oneLineAboveText, beforeText, afterText); + let testIndentAction = (previousLineText: string, beforeText: string, afterText: string, expectedIndentAction: IndentAction | null, expectedAppendText: string | null, removeText: number = 0) => { + let actual = support.onEnter(EditorAutoIndentStrategy.Advanced, previousLineText, beforeText, afterText); if (expectedIndentAction === null) { - assert.equal(actual, null, 'isNull:' + beforeText); + assert.strictEqual(actual, null, 'isNull:' + beforeText); } else { - assert.equal(actual !== null, true, 'isNotNull:' + beforeText); - assert.equal(actual!.indentAction, expectedIndentAction, 'indentAction:' + beforeText); + assert.strictEqual(actual !== null, true, 'isNotNull:' + beforeText); + assert.strictEqual(actual!.indentAction, expectedIndentAction, 'indentAction:' + beforeText); if (expectedAppendText !== null) { - assert.equal(actual!.appendText, expectedAppendText, 'appendText:' + beforeText); + assert.strictEqual(actual!.appendText, expectedAppendText, 'appendText:' + beforeText); } if (removeText !== 0) { - assert.equal(actual!.removeText, removeText, 'removeText:' + beforeText); + assert.strictEqual(actual!.removeText, removeText, 'removeText:' + beforeText); } } }; diff --git a/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts b/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts index 40ae7e628..cf6bb9997 100644 --- a/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts +++ b/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts @@ -19,61 +19,61 @@ suite('richEditBrackets', () => { test('findPrevBracketInToken one char 1', () => { let result = findPrevBracketInRange(/(\{)|(\})/i, '{', 0, 1); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 2); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 2); }); test('findPrevBracketInToken one char 2', () => { let result = findPrevBracketInRange(/(\{)|(\})/i, '{{', 0, 1); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 2); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 2); }); test('findPrevBracketInToken one char 3', () => { let result = findPrevBracketInRange(/(\{)|(\})/i, '{hello world!', 0, 13); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 2); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 2); }); test('findPrevBracketInToken more chars 1', () => { let result = findPrevBracketInRange(/(olleh)/i, 'hello world!', 0, 12); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 6); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 6); }); test('findPrevBracketInToken more chars 2', () => { let result = findPrevBracketInRange(/(olleh)/i, 'hello world!', 0, 5); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 6); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 6); }); test('findPrevBracketInToken more chars 3', () => { let result = findPrevBracketInRange(/(olleh)/i, ' hello world!', 0, 6); - assert.equal(result!.startColumn, 2); - assert.equal(result!.endColumn, 7); + assert.strictEqual(result!.startColumn, 2); + assert.strictEqual(result!.endColumn, 7); }); test('findNextBracketInToken one char', () => { let result = findNextBracketInRange(/(\{)|(\})/i, '{', 0, 1); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 2); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 2); }); test('findNextBracketInToken more chars', () => { let result = findNextBracketInRange(/(world)/i, 'hello world!', 0, 12); - assert.equal(result!.startColumn, 7); - assert.equal(result!.endColumn, 12); + assert.strictEqual(result!.startColumn, 7); + assert.strictEqual(result!.endColumn, 12); }); test('findNextBracketInToken with emoty result', () => { let result = findNextBracketInRange(/(\{)|(\})/i, '', 0, 0); - assert.equal(result, null); + assert.strictEqual(result, null); }); test('issue #3894: [Handlebars] Curly braces edit issues', () => { let result = findPrevBracketInRange(/(\-\-!<)|(>\-\-)|(\{\{)|(\}\})/i, '{{asd}}', 0, 2); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 3); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 3); }); }); diff --git a/src/vs/editor/test/common/modes/supports/tokenization.test.ts b/src/vs/editor/test/common/modes/supports/tokenization.test.ts index 9f2bd1b99..a8f904085 100644 --- a/src/vs/editor/test/common/modes/supports/tokenization.test.ts +++ b/src/vs/editor/test/common/modes/supports/tokenization.test.ts @@ -24,7 +24,7 @@ suite('Token theme matching', () => { let actual = theme._match('punctuation.definition.string.begin.html'); - assert.deepEqual(actual, new ThemeTrieElementRule(FontStyle.None, _D, _B)); + assert.deepStrictEqual(actual, new ThemeTrieElementRule(FontStyle.None, _D, _B)); }); test('can match', () => { @@ -55,7 +55,7 @@ suite('Token theme matching', () => { function assertMatch(scopeName: string, expected: ThemeTrieElementRule): void { let actual = theme._match(scopeName); - assert.deepEqual(actual, expected, 'when matching <<' + scopeName + '>>'); + assert.deepStrictEqual(actual, expected, 'when matching <<' + scopeName + '>>'); } function assertSimpleMatch(scopeName: string, fontStyle: FontStyle, foreground: number, background: number): void { @@ -152,7 +152,7 @@ suite('Token theme parsing', () => { new ParsedTokenThemeRule('constant.numeric.dec', 10, FontStyle.None, '0000ff', null), ]; - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); }); @@ -162,7 +162,7 @@ suite('Token theme resolving', () => { let actual = ['bar', 'z', 'zu', 'a', 'ab', ''].sort(strcmp); let expected = ['', 'a', 'ab', 'bar', 'z', 'zu']; - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); test('always has defaults', () => { @@ -170,8 +170,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('respects incoming defaults 1', () => { @@ -181,8 +181,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('respects incoming defaults 2', () => { @@ -192,8 +192,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('respects incoming defaults 3', () => { @@ -203,8 +203,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _A, _B))); }); test('respects incoming defaults 4', () => { @@ -214,8 +214,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('ff0000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('respects incoming defaults 5', () => { @@ -225,8 +225,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ff0000'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('can merge incoming defaults', () => { @@ -238,8 +238,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('00ff00'); const _B = colorMap.getId('ff0000'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _A, _B))); }); test('defaults are inherited', () => { @@ -251,7 +251,7 @@ suite('Token theme resolving', () => { const _A = colorMap.getId('F8F8F2'); const _B = colorMap.getId('272822'); const _C = colorMap.getId('ff0000'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); let root = new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B), { 'var': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _C, _B)) }); @@ -268,7 +268,7 @@ suite('Token theme resolving', () => { const _A = colorMap.getId('F8F8F2'); const _B = colorMap.getId('272822'); const _C = colorMap.getId('ff0000'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); let root = new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B), { 'var': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _C, _B)) }); @@ -286,7 +286,7 @@ suite('Token theme resolving', () => { const _B = colorMap.getId('272822'); const _C = colorMap.getId('ff0000'); const _D = colorMap.getId('00ff00'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); let root = new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B), { 'var': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _C, _B), { 'identifier': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _D, _B)) @@ -314,7 +314,7 @@ suite('Token theme resolving', () => { const _E = colorMap.getId('300000'); const _F = colorMap.getId('ff0000'); const _G = colorMap.getId('00ff00'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); let root = new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B), { 'var': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _F, _B), { 'identifier': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _G, _B)) @@ -341,6 +341,6 @@ suite('Token theme resolving', () => { colorMap.getId('FFFFFF'); colorMap.getId('0F0F0F'); colorMap.getId('F8F8F2'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); }); }); diff --git a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts index fb4c4028b..aaa8f060f 100644 --- a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts +++ b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts @@ -31,7 +31,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ]; let expectedStr = `
    ${toStr(expected)}
    `; - assert.equal(actual, expectedStr); + assert.strictEqual(actual, expectedStr); mode.dispose(); }); @@ -61,7 +61,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { let expectedStr2 = toStr(expected2); let expectedStr = `
    ${expectedStr1}
    ${expectedStr2}
    `; - assert.equal(actual, expectedStr); + assert.strictEqual(actual, expectedStr); mode.dispose(); }); @@ -104,7 +104,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ]); const colorMap = [null!, '#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff']; - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 17, 4, true), [ '
    ', @@ -117,7 +117,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 12, 4, true), [ '
    ', @@ -130,7 +130,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 11, 4, true), [ '
    ', @@ -142,7 +142,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 1, 11, 4, true), [ '
    ', @@ -154,7 +154,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 4, 11, 4, true), [ '
    ', @@ -165,7 +165,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 5, 11, 4, true), [ '
    ', @@ -175,7 +175,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 5, 10, 4, true), [ '
    ', @@ -184,7 +184,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 6, 9, 4, true), [ '
    ', @@ -237,7 +237,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ]); const colorMap = [null!, '#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff']; - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 21, 4, true), [ '
    ', @@ -251,7 +251,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 17, 4, true), [ '
    ', @@ -265,7 +265,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 3, 4, true), [ '
    ', @@ -287,7 +287,7 @@ class Mode extends MockMode { this._register(TokenizationRegistry.register(this.getId(), { getInitialState: (): IState => null!, tokenize: undefined!, - tokenize2: (line: string, state: IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { let tokensArr: number[] = []; let prevColor: ColorId = -1; for (let i = 0; i < line.length; i++) { diff --git a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts index 54f73ed0e..bcfafe9d0 100644 --- a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts +++ b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts @@ -43,13 +43,13 @@ suite('EditorSimpleWorker', () => { function assertPositionAt(offset: number, line: number, column: number) { let position = model.positionAt(offset); - assert.equal(position.lineNumber, line); - assert.equal(position.column, column); + assert.strictEqual(position.lineNumber, line); + assert.strictEqual(position.column, column); } function assertOffsetAt(lineNumber: number, column: number, offset: number) { let actual = model.offsetAt({ lineNumber, column }); - assert.equal(actual, offset); + assert.strictEqual(actual, offset); } test('ICommonModel#offsetAt', () => { @@ -83,16 +83,16 @@ suite('EditorSimpleWorker', () => { test('ICommonModel#validatePosition, issue #15882', function () { let model = worker.addModel(['{"id": "0001","type": "donut","name": "Cake","image":{"url": "images/0001.jpg","width": 200,"height": 200},"thumbnail":{"url": "images/thumbnails/0001.jpg","width": 32,"height": 32}}']); - assert.equal(model.offsetAt({ lineNumber: 1, column: 2 }), 1); + assert.strictEqual(model.offsetAt({ lineNumber: 1, column: 2 }), 1); }); test('MoreMinimal', () => { return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: 'This is line One', range: new Range(1, 1, 1, 17) }]).then(edits => { - assert.equal(edits.length, 1); + assert.strictEqual(edits.length, 1); const [first] = edits; - assert.equal(first.text, 'O'); - assert.deepEqual(first.range, { startLineNumber: 1, startColumn: 14, endLineNumber: 1, endColumn: 15 }); + assert.strictEqual(first.text, 'O'); + assert.deepStrictEqual(first.range, { startLineNumber: 1, startColumn: 14, endLineNumber: 1, endColumn: 15 }); }); }); @@ -105,7 +105,7 @@ suite('EditorSimpleWorker', () => { ], '\n'); return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '{\r\n\t"a":1\r\n}', range: new Range(1, 1, 3, 2) }]).then(edits => { - assert.equal(edits.length, 0); + assert.strictEqual(edits.length, 0); }); }); @@ -118,10 +118,10 @@ suite('EditorSimpleWorker', () => { ], '\n'); return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '{\r\n\t"b":1\r\n}', range: new Range(1, 1, 3, 2) }]).then(edits => { - assert.equal(edits.length, 1); + assert.strictEqual(edits.length, 1); const [first] = edits; - assert.equal(first.text, 'b'); - assert.deepEqual(first.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 2, endColumn: 4 }); + assert.strictEqual(first.text, 'b'); + assert.deepStrictEqual(first.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 2, endColumn: 4 }); }); }); @@ -134,10 +134,10 @@ suite('EditorSimpleWorker', () => { ]); return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '\n', range: new Range(3, 2, 4, 1000) }]).then(edits => { - assert.equal(edits.length, 1); + assert.strictEqual(edits.length, 1); const [first] = edits; - assert.equal(first.text, '\n'); - assert.deepEqual(first.range, { startLineNumber: 3, startColumn: 2, endLineNumber: 3, endColumn: 2 }); + assert.strictEqual(first.text, '\n'); + assert.deepStrictEqual(first.range, { startLineNumber: 3, startColumn: 2, endLineNumber: 3, endColumn: 2 }); }); }); @@ -151,7 +151,7 @@ suite('EditorSimpleWorker', () => { ]); const value = model.getValueInRange({ startLineNumber: 3, startColumn: 1, endLineNumber: 4, endColumn: 1 }); - assert.equal(value, '}'); + assert.strictEqual(value, '}'); }); @@ -165,11 +165,10 @@ suite('EditorSimpleWorker', () => { return worker.textualSuggest([model.uri.toString()], 'f', '[a-z]+', 'img').then((result) => { if (!result) { assert.ok(false); - return; } - assert.equal(result.words.length, 1); - assert.equal(typeof result.duration, 'number'); - assert.equal(result.words[0], 'foobar'); + assert.strictEqual(result.words.length, 1); + assert.strictEqual(typeof result.duration, 'number'); + assert.strictEqual(result.words[0], 'foobar'); }); }); @@ -187,6 +186,6 @@ suite('EditorSimpleWorker', () => { let words: string[] = [...model.words(/[a-z]+/img)]; - assert.deepEqual(words, ['one', 'line', 'two', 'line', 'past', 'empty', 'single', 'and', 'now', 'we', 'are', 'done']); + assert.deepStrictEqual(words, ['one', 'line', 'two', 'line', 'past', 'empty', 'single', 'and', 'now', 'we', 'are', 'done']); }); }); diff --git a/src/vs/editor/test/common/services/languagesRegistry.test.ts b/src/vs/editor/test/common/services/languagesRegistry.test.ts index 09ef74cd0..d0eac05b5 100644 --- a/src/vs/editor/test/common/services/languagesRegistry.test.ts +++ b/src/vs/editor/test/common/services/languagesRegistry.test.ts @@ -19,7 +19,7 @@ suite('LanguagesRegistry', () => { mimetypes: ['outputModeMimeType'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), []); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), []); }); test('mode with alias does have a name', () => { @@ -32,8 +32,8 @@ suite('LanguagesRegistry', () => { mimetypes: ['bla'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['ModeName']); - assert.deepEqual(registry.getLanguageName('modeId'), 'ModeName'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['ModeName']); + assert.deepStrictEqual(registry.getLanguageName('modeId'), 'ModeName'); }); test('mode without alias gets a name', () => { @@ -45,8 +45,8 @@ suite('LanguagesRegistry', () => { mimetypes: ['bla'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['modeId']); - assert.deepEqual(registry.getLanguageName('modeId'), 'modeId'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['modeId']); + assert.deepStrictEqual(registry.getLanguageName('modeId'), 'modeId'); }); test('bug #4360: f# not shown in status bar', () => { @@ -66,8 +66,8 @@ suite('LanguagesRegistry', () => { mimetypes: ['bla'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['ModeName']); - assert.deepEqual(registry.getLanguageName('modeId'), 'ModeName'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['ModeName']); + assert.deepStrictEqual(registry.getLanguageName('modeId'), 'ModeName'); }); test('issue #5278: Extension cannot override language name anymore', () => { @@ -87,8 +87,8 @@ suite('LanguagesRegistry', () => { mimetypes: ['bla'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['BetterModeName']); - assert.deepEqual(registry.getLanguageName('modeId'), 'BetterModeName'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['BetterModeName']); + assert.deepStrictEqual(registry.getLanguageName('modeId'), 'BetterModeName'); }); test('mimetypes are generated if necessary', () => { @@ -98,7 +98,7 @@ suite('LanguagesRegistry', () => { id: 'modeId' }]); - assert.deepEqual(registry.getMimeForMode('modeId'), 'text/x-modeId'); + assert.deepStrictEqual(registry.getMimeForMode('modeId'), 'text/x-modeId'); }); test('first mimetype wins', () => { @@ -109,7 +109,7 @@ suite('LanguagesRegistry', () => { mimetypes: ['text/modeId', 'text/modeId2'] }]); - assert.deepEqual(registry.getMimeForMode('modeId'), 'text/modeId'); + assert.deepStrictEqual(registry.getMimeForMode('modeId'), 'text/modeId'); }); test('first mimetype wins 2', () => { @@ -124,7 +124,7 @@ suite('LanguagesRegistry', () => { mimetypes: ['text/modeId'] }]); - assert.deepEqual(registry.getMimeForMode('modeId'), 'text/x-modeId'); + assert.deepStrictEqual(registry.getMimeForMode('modeId'), 'text/x-modeId'); }); test('aliases', () => { @@ -134,42 +134,42 @@ suite('LanguagesRegistry', () => { id: 'a' }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), ['a']); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getLanguageName('a'), 'a'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), ['a']); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'a'); registry._registerLanguages([{ id: 'a', aliases: ['A1', 'A2'] }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['A1']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), []); - assert.deepEqual(registry.getModeIdsFromLanguageName('A1'), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('A2'), []); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a1'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a2'), 'a'); - assert.deepEqual(registry.getLanguageName('a'), 'A1'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['A1']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), []); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A1'), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A2'), []); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a1'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a2'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'A1'); registry._registerLanguages([{ id: 'a', aliases: ['A3', 'A4'] }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['A3']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), []); - assert.deepEqual(registry.getModeIdsFromLanguageName('A1'), []); - assert.deepEqual(registry.getModeIdsFromLanguageName('A2'), []); - assert.deepEqual(registry.getModeIdsFromLanguageName('A3'), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('A4'), []); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a1'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a2'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a3'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a4'), 'a'); - assert.deepEqual(registry.getLanguageName('a'), 'A3'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['A3']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), []); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A1'), []); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A2'), []); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A3'), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A4'), []); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a1'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a2'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a3'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a4'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'A3'); }); test('empty aliases array means no alias', () => { @@ -179,23 +179,23 @@ suite('LanguagesRegistry', () => { id: 'a' }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), ['a']); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getLanguageName('a'), 'a'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), ['a']); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'a'); registry._registerLanguages([{ id: 'b', aliases: [] }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('b'), []); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('b'), 'b'); - assert.deepEqual(registry.getLanguageName('a'), 'a'); - assert.deepEqual(registry.getLanguageName('b'), null); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('b'), []); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('b'), 'b'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('b'), null); }); test('extensions', () => { @@ -207,18 +207,18 @@ suite('LanguagesRegistry', () => { extensions: ['aExt'] }]); - assert.deepEqual(registry.getExtensions('a'), []); - assert.deepEqual(registry.getExtensions('aname'), []); - assert.deepEqual(registry.getExtensions('aName'), ['aExt']); + assert.deepStrictEqual(registry.getExtensions('a'), []); + assert.deepStrictEqual(registry.getExtensions('aname'), []); + assert.deepStrictEqual(registry.getExtensions('aName'), ['aExt']); registry._registerLanguages([{ id: 'a', extensions: ['aExt2'] }]); - assert.deepEqual(registry.getExtensions('a'), []); - assert.deepEqual(registry.getExtensions('aname'), []); - assert.deepEqual(registry.getExtensions('aName'), ['aExt', 'aExt2']); + assert.deepStrictEqual(registry.getExtensions('a'), []); + assert.deepStrictEqual(registry.getExtensions('aname'), []); + assert.deepStrictEqual(registry.getExtensions('aName'), ['aExt', 'aExt2']); }); test('extensions of primary language registration come first', () => { @@ -229,7 +229,7 @@ suite('LanguagesRegistry', () => { extensions: ['aExt3'] }]); - assert.deepEqual(registry.getExtensions('a')[0], 'aExt3'); + assert.deepStrictEqual(registry.getExtensions('a')[0], 'aExt3'); registry._registerLanguages([{ id: 'a', @@ -237,14 +237,14 @@ suite('LanguagesRegistry', () => { extensions: ['aExt'] }]); - assert.deepEqual(registry.getExtensions('a')[0], 'aExt'); + assert.deepStrictEqual(registry.getExtensions('a')[0], 'aExt'); registry._registerLanguages([{ id: 'a', extensions: ['aExt2'] }]); - assert.deepEqual(registry.getExtensions('a')[0], 'aExt'); + assert.deepStrictEqual(registry.getExtensions('a')[0], 'aExt'); }); test('filenames', () => { @@ -256,18 +256,18 @@ suite('LanguagesRegistry', () => { filenames: ['aFilename'] }]); - assert.deepEqual(registry.getFilenames('a'), []); - assert.deepEqual(registry.getFilenames('aname'), []); - assert.deepEqual(registry.getFilenames('aName'), ['aFilename']); + assert.deepStrictEqual(registry.getFilenames('a'), []); + assert.deepStrictEqual(registry.getFilenames('aname'), []); + assert.deepStrictEqual(registry.getFilenames('aName'), ['aFilename']); registry._registerLanguages([{ id: 'a', filenames: ['aFilename2'] }]); - assert.deepEqual(registry.getFilenames('a'), []); - assert.deepEqual(registry.getFilenames('aname'), []); - assert.deepEqual(registry.getFilenames('aName'), ['aFilename', 'aFilename2']); + assert.deepStrictEqual(registry.getFilenames('a'), []); + assert.deepStrictEqual(registry.getFilenames('aname'), []); + assert.deepStrictEqual(registry.getFilenames('aName'), ['aFilename', 'aFilename2']); }); test('configuration', () => { @@ -279,17 +279,17 @@ suite('LanguagesRegistry', () => { configuration: URI.file('/path/to/aFilename') }]); - assert.deepEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename')]); - assert.deepEqual(registry.getConfigurationFiles('aname'), []); - assert.deepEqual(registry.getConfigurationFiles('aName'), []); + assert.deepStrictEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename')]); + assert.deepStrictEqual(registry.getConfigurationFiles('aname'), []); + assert.deepStrictEqual(registry.getConfigurationFiles('aName'), []); registry._registerLanguages([{ id: 'a', configuration: URI.file('/path/to/aFilename2') }]); - assert.deepEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename'), URI.file('/path/to/aFilename2')]); - assert.deepEqual(registry.getConfigurationFiles('aname'), []); - assert.deepEqual(registry.getConfigurationFiles('aName'), []); + assert.deepStrictEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename'), URI.file('/path/to/aFilename2')]); + assert.deepStrictEqual(registry.getConfigurationFiles('aname'), []); + assert.deepStrictEqual(registry.getConfigurationFiles('aName'), []); }); }); diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index 52672ffe3..23e0f8f2a 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -11,18 +11,26 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { createStringBuilder } from 'vs/editor/common/core/stringBuilder'; -import { DefaultEndOfLine } from 'vs/editor/common/model'; +import { DefaultEndOfLine, ITextModel } from 'vs/editor/common/model'; import { createTextBuffer } from 'vs/editor/common/model/textModel'; -import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; -import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ModelSemanticColoring, ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { TestColorTheme, TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { NullLogService } from 'vs/platform/log/common/log'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DocumentSemanticTokensProvider, DocumentSemanticTokensProviderRegistry, SemanticTokens, SemanticTokensEdits, SemanticTokensLegend } from 'vs/editor/common/modes'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Barrier, timeout } from 'vs/base/common/async'; +import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; +import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; const GENERATE_TESTS = false; @@ -47,9 +55,9 @@ suite('ModelService', () => { const model2 = modelService.createModel('farboo', null, URI.file(platform.isWindows ? 'c:\\myroot\\myfile.txt' : '/myroot/myfile.txt')); const model3 = modelService.createModel('farboo', null, URI.file(platform.isWindows ? 'c:\\other\\myfile.txt' : '/other/myfile.txt')); - assert.equal(model1.getOptions().defaultEOL, DefaultEndOfLine.LF); - assert.equal(model2.getOptions().defaultEOL, DefaultEndOfLine.CRLF); - assert.equal(model3.getOptions().defaultEOL, DefaultEndOfLine.LF); + assert.strictEqual(model1.getOptions().defaultEOL, DefaultEndOfLine.LF); + assert.strictEqual(model2.getOptions().defaultEOL, DefaultEndOfLine.CRLF); + assert.strictEqual(model3.getOptions().defaultEOL, DefaultEndOfLine.LF); }); test('_computeEdits no change', function () { @@ -71,11 +79,11 @@ suite('ModelService', () => { 'and finished with the fourth.', //29 ].join('\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, []); + assert.deepStrictEqual(actual, []); }); test('_computeEdits first line changed', function () { @@ -97,11 +105,11 @@ suite('ModelService', () => { 'and finished with the fourth.', //29 ].join('\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ EditOperation.replaceMove(new Range(1, 1, 2, 1), 'This is line One\n') ]); }); @@ -125,11 +133,11 @@ suite('ModelService', () => { 'and finished with the fourth.', //29 ].join('\r\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, []); + assert.deepStrictEqual(actual, []); }); test('_computeEdits EOL and other change 1', function () { @@ -151,11 +159,11 @@ suite('ModelService', () => { 'and finished with the fourth.', //29 ].join('\r\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ EditOperation.replaceMove( new Range(1, 1, 4, 1), [ @@ -186,11 +194,11 @@ suite('ModelService', () => { '' ].join('\r\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ EditOperation.replaceMove(new Range(3, 2, 3, 2), '\r\n') ]); }); @@ -317,7 +325,7 @@ suite('ModelService', () => { const model1 = modelService.createModel('text', null, resource); // make an edit model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); - assert.equal(model1.getValue(), 'text1'); + assert.strictEqual(model1.getValue(), 'text1'); // dispose it modelService.destroyModel(resource); @@ -325,7 +333,7 @@ suite('ModelService', () => { const model2 = modelService.createModel('text1', null, resource); // undo model2.undo(); - assert.equal(model2.getValue(), 'text'); + assert.strictEqual(model2.getValue(), 'text'); }); test('maintains version id and alternative version id for same resource and same content', () => { @@ -335,7 +343,7 @@ suite('ModelService', () => { const model1 = modelService.createModel('text', null, resource); // make an edit model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); - assert.equal(model1.getValue(), 'text1'); + assert.strictEqual(model1.getValue(), 'text1'); const versionId = model1.getVersionId(); const alternativeVersionId = model1.getAlternativeVersionId(); // dispose it @@ -343,8 +351,8 @@ suite('ModelService', () => { // create a new model with the same content const model2 = modelService.createModel('text1', null, resource); - assert.equal(model2.getVersionId(), versionId); - assert.equal(model2.getAlternativeVersionId(), alternativeVersionId); + assert.strictEqual(model2.getVersionId(), versionId); + assert.strictEqual(model2.getAlternativeVersionId(), alternativeVersionId); }); test('does not maintain undo for same resource and different content', () => { @@ -354,7 +362,7 @@ suite('ModelService', () => { const model1 = modelService.createModel('text', null, resource); // make an edit model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); - assert.equal(model1.getValue(), 'text1'); + assert.strictEqual(model1.getValue(), 'text1'); // dispose it modelService.destroyModel(resource); @@ -362,7 +370,7 @@ suite('ModelService', () => { const model2 = modelService.createModel('text2', null, resource); // undo model2.undo(); - assert.equal(model2.getValue(), 'text2'); + assert.strictEqual(model2.getValue(), 'text2'); }); test('setValue should clear undo stack', () => { @@ -370,17 +378,98 @@ suite('ModelService', () => { const model = modelService.createModel('text', null, resource); model.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); - assert.equal(model.getValue(), 'text1'); + assert.strictEqual(model.getValue(), 'text1'); model.setValue('text2'); model.undo(); - assert.equal(model.getValue(), 'text2'); + assert.strictEqual(model.getValue(), 'text2'); + }); +}); + +suite('ModelSemanticColoring', () => { + + const disposables = new DisposableStore(); + const ORIGINAL_FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY = ModelSemanticColoring.FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY; + let modelService: IModelService; + let modeService: IModeService; + + setup(() => { + ModelSemanticColoring.FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY = 0; + + const configService = new TestConfigurationService({ editor: { semanticHighlighting: true } }); + const themeService = new TestThemeService(); + themeService.setTheme(new TestColorTheme({}, ColorScheme.DARK, true)); + modelService = disposables.add(new ModelServiceImpl( + configService, + new TestTextResourcePropertiesService(configService), + themeService, + new NullLogService(), + new UndoRedoService(new TestDialogService(), new TestNotificationService()) + )); + modeService = disposables.add(new ModeServiceImpl(false)); + }); + + teardown(() => { + disposables.clear(); + ModelSemanticColoring.FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY = ORIGINAL_FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY; + }); + + test('DocumentSemanticTokens should be fetched when the result is empty if there are pending changes', async () => { + + disposables.add(ModesRegistry.registerLanguage({ id: 'testMode' })); + + const inFirstCall = new Barrier(); + const delayFirstResult = new Barrier(); + const secondResultProvided = new Barrier(); + let callCount = 0; + + disposables.add(DocumentSemanticTokensProviderRegistry.register('testMode', new class implements DocumentSemanticTokensProvider { + getLegend(): SemanticTokensLegend { + return { tokenTypes: ['class'], tokenModifiers: [] }; + } + async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { + callCount++; + if (callCount === 1) { + assert.ok('called once'); + inFirstCall.open(); + await delayFirstResult.wait(); + await timeout(0); // wait for the simple scheduler to fire to check that we do actually get rescheduled + return null; + } + if (callCount === 2) { + assert.ok('called twice'); + secondResultProvided.open(); + return null; + } + assert.fail('Unexpected call'); + } + releaseDocumentSemanticTokens(resultId: string | undefined): void { + } + })); + + const textModel = disposables.add(modelService.createModel('Hello world', modeService.create('testMode'))); + + // wait for the provider to be called + await inFirstCall.wait(); + + // the provider is now in the provide call + // change the text buffer while the provider is running + textModel.applyEdits([{ range: new Range(1, 1, 1, 1), text: 'x' }]); + + // let the provider finish its first result + delayFirstResult.open(); + + // we need to check that the provider is called again, even if it returns null + await secondResultProvided.wait(); + + // assert that it got called twice + assert.strictEqual(callCount, 2); }); }); function assertComputeEdits(lines1: string[], lines2: string[]): void { const model = createTextModel(lines1.join('\n')); - const textBuffer = createTextBuffer(lines2.join('\n'), DefaultEndOfLine.LF); + const textBuffer = createTextBuffer(lines2.join('\n'), DefaultEndOfLine.LF).textBuffer; // compute required edits // let start = Date.now(); @@ -390,7 +479,7 @@ function assertComputeEdits(lines1: string[], lines2: string[]): void { // apply edits model.pushEditOperations([], edits, null); - assert.equal(model.getValue(), lines2.join('\n')); + assert.strictEqual(model.getValue(), lines2.join('\n')); } function getRandomInt(min: number, max: number): number { @@ -439,21 +528,3 @@ assertComputeEdits(file1, file2); } } } - -export class TestTextResourcePropertiesService implements ITextResourcePropertiesService { - - declare readonly _serviceBrand: undefined; - - constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, - ) { - } - - getEOL(resource: URI, language?: string): string { - const eol = this.configurationService.getValue('files.eol', { overrideIdentifier: language, resource }); - if (eol && eol !== 'auto') { - return eol; - } - return (platform.isLinux || platform.isMacintosh) ? '\n' : '\r\n'; - } -} diff --git a/src/vs/workbench/test/common/api/semanticTokensDto.test.ts b/src/vs/editor/test/common/services/semanticTokensDto.test.ts similarity index 95% rename from src/vs/workbench/test/common/api/semanticTokensDto.test.ts rename to src/vs/editor/test/common/services/semanticTokensDto.test.ts index 091bc24e2..9cedebd29 100644 --- a/src/vs/workbench/test/common/api/semanticTokensDto.test.ts +++ b/src/vs/editor/test/common/services/semanticTokensDto.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IFullSemanticTokensDto, IDeltaSemanticTokensDto, encodeSemanticTokensDto, ISemanticTokensDto, decodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto'; +import { IFullSemanticTokensDto, IDeltaSemanticTokensDto, encodeSemanticTokensDto, ISemanticTokensDto, decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; import { VSBuffer } from 'vs/base/common/buffer'; suite('SemanticTokensDto', () => { @@ -25,7 +25,7 @@ suite('SemanticTokensDto', () => { data: toArr(dto.data) }; }; - assert.deepEqual(convert(actual), convert(expected)); + assert.deepStrictEqual(convert(actual), convert(expected)); } function assertEqualDelta(actual: IDeltaSemanticTokensDto, expected: IDeltaSemanticTokensDto): void { @@ -46,7 +46,7 @@ suite('SemanticTokensDto', () => { deltas: dto.deltas.map(convertOne) }; }; - assert.deepEqual(convert(actual), convert(expected)); + assert.deepStrictEqual(convert(actual), convert(expected)); } function testRoundTrip(value: ISemanticTokensDto): void { diff --git a/src/vs/editor/test/common/services/testTextResourcePropertiesService.ts b/src/vs/editor/test/common/services/testTextResourcePropertiesService.ts new file mode 100644 index 000000000..f5ac31d5f --- /dev/null +++ b/src/vs/editor/test/common/services/testTextResourcePropertiesService.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as platform from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; +import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +export class TestTextResourcePropertiesService implements ITextResourcePropertiesService { + + declare readonly _serviceBrand: undefined; + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + } + + getEOL(resource: URI, language?: string): string { + const eol = this.configurationService.getValue('files.eol', { overrideIdentifier: language, resource }); + if (eol && eol !== 'auto') { + return eol; + } + return (platform.isLinux || platform.isMacintosh) ? '\n' : '\r\n'; + } +} diff --git a/src/vs/editor/test/common/view/overviewZoneManager.test.ts b/src/vs/editor/test/common/view/overviewZoneManager.test.ts index 39c104fbb..ee8c8ed7d 100644 --- a/src/vs/editor/test/common/view/overviewZoneManager.test.ts +++ b/src/vs/editor/test/common/view/overviewZoneManager.test.ts @@ -26,7 +26,7 @@ suite('Editor View - OverviewZoneManager', () => { ]); // one line = 12, but cap is at 6 - assert.deepEqual(manager.resolveColorZones(), [ + assert.deepStrictEqual(manager.resolveColorZones(), [ new ColorZone(12, 24, 1), // new ColorZone(120, 132, 2), // 120 -> 132 new ColorZone(360, 384, 3), // 360 -> 372 [360 -> 384] @@ -52,7 +52,7 @@ suite('Editor View - OverviewZoneManager', () => { ]); // one line = 6, cap is at 6 - assert.deepEqual(manager.resolveColorZones(), [ + assert.deepStrictEqual(manager.resolveColorZones(), [ new ColorZone(6, 12, 1), // new ColorZone(60, 66, 2), // 60 -> 66 new ColorZone(180, 192, 3), // 180 -> 192 @@ -78,7 +78,7 @@ suite('Editor View - OverviewZoneManager', () => { ]); // one line = 6, cap is at 12 - assert.deepEqual(manager.resolveColorZones(), [ + assert.deepStrictEqual(manager.resolveColorZones(), [ new ColorZone(12, 24, 1), // new ColorZone(120, 132, 2), // 120 -> 132 new ColorZone(360, 384, 3), // 360 -> 384 diff --git a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts index 910165a0e..a0fa69bc8 100644 --- a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts @@ -95,7 +95,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { maxDigitWidth: input.maxDigitWidth, pixelRatio: input.pixelRatio, }); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('EditorLayoutProvider 1', () => { diff --git a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts index 8be7627bc..e8939e784 100644 --- a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts +++ b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts @@ -17,7 +17,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]); - assert.deepEqual(result, [ + assert.deepStrictEqual(result, [ new DecorationSegment(0, 1, 'c1', 0), new DecorationSegment(2, 2, 'c2 c1', 0), new DecorationSegment(3, 9, 'c1', 0), @@ -31,7 +31,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new LineDecoration(20, 21, 'inline-folded', InlineDecorationType.Regular), ]); - assert.deepEqual(result, [ + assert.deepStrictEqual(result, [ new DecorationSegment(14, 18, 'mtkw', 0), new DecorationSegment(19, 19, 'mtkw inline-folded', 0) ]); @@ -43,7 +43,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new InlineDecoration(new Range(2, 12, 3, 30), 'detected-link', InlineDecorationType.Regular) ], 3, 12, 500); - assert.deepEqual(result, [ + assert.deepStrictEqual(result, [ new LineDecoration(12, 30, 'detected-link', InlineDecorationType.Regular), ]); }); @@ -54,7 +54,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new InlineDecoration(new Range(4, 0, 4, 1), 'after', InlineDecorationType.After), ], 4, 1, 500); - assert.deepEqual(result, [ + assert.deepStrictEqual(result, [ new LineDecoration(1, 2, 'before', InlineDecorationType.Before), new LineDecoration(0, 1, 'after', InlineDecorationType.After), ]); @@ -62,7 +62,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { test('ViewLineParts', () => { - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 2, 'c1', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]), [ @@ -70,7 +70,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 3, 'c1', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]), [ @@ -78,7 +78,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]), [ @@ -86,7 +86,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c1 c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1*', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) @@ -95,7 +95,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c1 c1* c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1*', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1**', InlineDecorationType.Regular), @@ -105,7 +105,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c1 c1* c1** c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1*', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1**', InlineDecorationType.Regular), @@ -116,7 +116,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c1 c1* c1** c2 c2*', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1*', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1**', InlineDecorationType.Regular), diff --git a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts index 3205a10ce..6d619cf60 100644 --- a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts +++ b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts @@ -34,105 +34,105 @@ suite('Editor ViewLayout - LinesLayout', () => { // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: - - assert.equal(linesLayout.getLinesTotalHeight(), 100); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 30); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 50); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 60); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 70); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 80); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 90); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 100); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 30); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 50); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 60); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 70); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 80); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 90); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(5), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(11), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(29), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(5), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(11), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(29), 3); // Add whitespace of height 5px after 2nd line insertWhitespace(linesLayout, 2, 0, 5, 0); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: a(2,5) - assert.equal(linesLayout.getLinesTotalHeight(), 105); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 35); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 45); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 105); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 35); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 45); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(45), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(104), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(105), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(45), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(104), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(105), 10); // Add two more whitespaces of height 5px insertWhitespace(linesLayout, 3, 0, 5, 0); insertWhitespace(linesLayout, 4, 0, 5, 0); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: a(2,5), b(3, 5), c(4, 5) - assert.equal(linesLayout.getLinesTotalHeight(), 115); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 55); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 65); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 115); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 55); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 65); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(64), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(65), 6); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(64), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(65), 6); - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(0), 20); // 20 -> 25 - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(1), 35); // 35 -> 40 - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(2), 50); + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(0), 20); // 20 -> 25 + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(1), 35); // 35 -> 40 + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(2), 50); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(0), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(19), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(20), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(21), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(22), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(23), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(24), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(25), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(26), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(34), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(35), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(36), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(39), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(40), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(41), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(49), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(50), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(51), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(54), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(55), -1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(1000), -1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(0), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(19), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(20), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(21), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(22), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(23), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(24), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(25), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(26), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(34), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(35), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(36), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(39), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(40), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(41), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(49), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(50), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(51), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(54), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(55), -1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(1000), -1); }); @@ -144,94 +144,94 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(2,5) - assert.equal(linesLayout.getLinesTotalHeight(), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 7); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 8); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 9); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 11); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 12); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 13); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 14); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 7); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 8); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 9); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 11); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 12); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 13); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 14); // Change whitespace height // 10 lines // whitespace: - a(2,10) changeOneWhitespace(linesLayout, a, 2, 10); - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 12); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 13); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 14); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 12); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 13); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 14); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Change whitespace position // 10 lines // whitespace: - a(5,10) changeOneWhitespace(linesLayout, a, 5, 10); - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Pretend that lines 5 and 6 were deleted // 8 lines // whitespace: - a(4,10) linesLayout.onLinesDeleted(5, 6); - assert.equal(linesLayout.getLinesTotalHeight(), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 14); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 14); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); // Insert two lines at the beginning // 10 lines // whitespace: - a(6,10) linesLayout.onLinesInserted(1, 2); - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 5); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 5); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Remove whitespace // 10 lines removeWhitespace(linesLayout, a); - assert.equal(linesLayout.getLinesTotalHeight(), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 5); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 6); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 7); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 8); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 9); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 5); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 6); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 7); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 8); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 9); }); test('LinesLayout Padding', () => { @@ -240,93 +240,93 @@ suite('Editor ViewLayout - LinesLayout', () => { // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: - - assert.equal(linesLayout.getLinesTotalHeight(), 135); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 35); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 45); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 55); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 65); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 75); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 85); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 95); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 105); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 135); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 35); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 45); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 55); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 65); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 75); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 85); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 95); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 105); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); // Add whitespace of height 5px after 2nd line insertWhitespace(linesLayout, 2, 0, 5, 0); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: a(2,5) - assert.equal(linesLayout.getLinesTotalHeight(), 140); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 50); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 140); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 50); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(39), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(40), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(41), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(39), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(40), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(41), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 4); // Add two more whitespaces of height 5px insertWhitespace(linesLayout, 3, 0, 5, 0); insertWhitespace(linesLayout, 4, 0, 5, 0); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: a(2,5), b(3, 5), c(4, 5) - assert.equal(linesLayout.getLinesTotalHeight(), 150); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 55); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 70); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 80); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 150); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 55); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 70); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 80); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(30), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(39), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(40), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(54), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(55), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(64), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(65), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(69), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(70), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(80), 6); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(30), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(39), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(40), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(54), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(55), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(64), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(65), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(69), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(70), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(80), 6); - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(0), 35); // 35 -> 40 - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(1), 50); // 50 -> 55 - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(2), 65); + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(0), 35); // 35 -> 40 + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(1), 50); // 50 -> 55 + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(2), 65); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(0), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(34), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(35), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(39), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(40), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(49), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(50), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(54), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(55), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(64), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(65), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(70), -1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(0), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(34), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(35), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(39), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(40), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(49), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(50), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(54), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(55), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(64), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(65), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(70), -1); }); test('LinesLayout getLineNumberAtOrAfterVerticalOffset', () => { @@ -335,47 +335,47 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(6,10) - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 5); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 5); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Do some hit testing // line [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // vertical: [0, 1, 2, 3, 4, 5, 16, 17, 18, 19] - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(-100), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(-1), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(2), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(3), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(4), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(5), 6); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(6), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(7), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(8), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(11), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(12), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(13), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(14), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(16), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(17), 8); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(18), 9); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(22), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(23), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(-100), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(-1), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(2), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(3), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(4), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(5), 6); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(6), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(7), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(8), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(11), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(12), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(13), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(14), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(16), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(17), 8); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(18), 9); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(22), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(23), 10); }); test('LinesLayout getCenteredLineInViewport', () => { @@ -384,81 +384,81 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(6,10) - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 5); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 5); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Find centered line in viewport 1 // line [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // vertical: [0, 1, 2, 3, 4, 5, 16, 17, 18, 19] - assert.equal(linesLayout.getLinesViewportData(0, 1).centeredLineNumber, 1); - assert.equal(linesLayout.getLinesViewportData(0, 2).centeredLineNumber, 2); - assert.equal(linesLayout.getLinesViewportData(0, 3).centeredLineNumber, 2); - assert.equal(linesLayout.getLinesViewportData(0, 4).centeredLineNumber, 3); - assert.equal(linesLayout.getLinesViewportData(0, 5).centeredLineNumber, 3); - assert.equal(linesLayout.getLinesViewportData(0, 6).centeredLineNumber, 4); - assert.equal(linesLayout.getLinesViewportData(0, 7).centeredLineNumber, 4); - assert.equal(linesLayout.getLinesViewportData(0, 8).centeredLineNumber, 5); - assert.equal(linesLayout.getLinesViewportData(0, 9).centeredLineNumber, 5); - assert.equal(linesLayout.getLinesViewportData(0, 10).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 11).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 12).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 13).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 14).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 15).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 16).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 17).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 18).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 19).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 21).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 22).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 23).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 24).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 25).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 26).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 27).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 28).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 29).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 30).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 31).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 32).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 33).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 1).centeredLineNumber, 1); + assert.strictEqual(linesLayout.getLinesViewportData(0, 2).centeredLineNumber, 2); + assert.strictEqual(linesLayout.getLinesViewportData(0, 3).centeredLineNumber, 2); + assert.strictEqual(linesLayout.getLinesViewportData(0, 4).centeredLineNumber, 3); + assert.strictEqual(linesLayout.getLinesViewportData(0, 5).centeredLineNumber, 3); + assert.strictEqual(linesLayout.getLinesViewportData(0, 6).centeredLineNumber, 4); + assert.strictEqual(linesLayout.getLinesViewportData(0, 7).centeredLineNumber, 4); + assert.strictEqual(linesLayout.getLinesViewportData(0, 8).centeredLineNumber, 5); + assert.strictEqual(linesLayout.getLinesViewportData(0, 9).centeredLineNumber, 5); + assert.strictEqual(linesLayout.getLinesViewportData(0, 10).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 11).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 12).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 13).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 14).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 15).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 16).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 17).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 18).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 19).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 21).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 22).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 23).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 24).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 25).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 26).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 27).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 28).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 29).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 30).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 31).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 32).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 33).centeredLineNumber, 7); // Find centered line in viewport 2 // line [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // vertical: [0, 1, 2, 3, 4, 5, 16, 17, 18, 19] - assert.equal(linesLayout.getLinesViewportData(0, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(1, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(2, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(3, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(4, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(5, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(6, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(7, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(8, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(9, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(10, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(11, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(12, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(13, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(14, 20).centeredLineNumber, 8); - assert.equal(linesLayout.getLinesViewportData(15, 20).centeredLineNumber, 8); - assert.equal(linesLayout.getLinesViewportData(16, 20).centeredLineNumber, 9); - assert.equal(linesLayout.getLinesViewportData(17, 20).centeredLineNumber, 9); - assert.equal(linesLayout.getLinesViewportData(18, 20).centeredLineNumber, 10); - assert.equal(linesLayout.getLinesViewportData(19, 20).centeredLineNumber, 10); - assert.equal(linesLayout.getLinesViewportData(20, 23).centeredLineNumber, 10); - assert.equal(linesLayout.getLinesViewportData(21, 23).centeredLineNumber, 10); - assert.equal(linesLayout.getLinesViewportData(22, 23).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(0, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(1, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(2, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(3, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(4, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(5, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(6, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(7, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(8, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(9, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(10, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(11, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(12, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(13, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(14, 20).centeredLineNumber, 8); + assert.strictEqual(linesLayout.getLinesViewportData(15, 20).centeredLineNumber, 8); + assert.strictEqual(linesLayout.getLinesViewportData(16, 20).centeredLineNumber, 9); + assert.strictEqual(linesLayout.getLinesViewportData(17, 20).centeredLineNumber, 9); + assert.strictEqual(linesLayout.getLinesViewportData(18, 20).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(19, 20).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(20, 23).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(21, 23).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(22, 23).centeredLineNumber, 10); }); test('LinesLayout getLinesViewportData 1', () => { @@ -467,131 +467,131 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(6,100) - assert.equal(linesLayout.getLinesTotalHeight(), 200); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 30); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 50); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 160); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 170); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 180); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 190); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 200); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 30); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 50); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 160); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 170); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 180); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 190); // viewport 0->50 let viewportData = linesLayout.getLinesViewportData(0, 50); - assert.equal(viewportData.startLineNumber, 1); - assert.equal(viewportData.endLineNumber, 5); - assert.equal(viewportData.completelyVisibleStartLineNumber, 1); - assert.equal(viewportData.completelyVisibleEndLineNumber, 5); - assert.deepEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40]); + assert.strictEqual(viewportData.startLineNumber, 1); + assert.strictEqual(viewportData.endLineNumber, 5); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 1); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 5); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40]); // viewport 1->51 viewportData = linesLayout.getLinesViewportData(1, 51); - assert.equal(viewportData.startLineNumber, 1); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 2); - assert.equal(viewportData.completelyVisibleEndLineNumber, 5); - assert.deepEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40, 50]); + assert.strictEqual(viewportData.startLineNumber, 1); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 2); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 5); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40, 50]); // viewport 5->55 viewportData = linesLayout.getLinesViewportData(5, 55); - assert.equal(viewportData.startLineNumber, 1); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 2); - assert.equal(viewportData.completelyVisibleEndLineNumber, 5); - assert.deepEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40, 50]); + assert.strictEqual(viewportData.startLineNumber, 1); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 2); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 5); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40, 50]); // viewport 10->60 viewportData = linesLayout.getLinesViewportData(10, 60); - assert.equal(viewportData.startLineNumber, 2); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 2); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [10, 20, 30, 40, 50]); + assert.strictEqual(viewportData.startLineNumber, 2); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 2); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [10, 20, 30, 40, 50]); // viewport 50->100 viewportData = linesLayout.getLinesViewportData(50, 100); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [50]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50]); // viewport 60->110 viewportData = linesLayout.getLinesViewportData(60, 110); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 65->115 viewportData = linesLayout.getLinesViewportData(65, 115); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 50->159 viewportData = linesLayout.getLinesViewportData(50, 159); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [50]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50]); // viewport 50->160 viewportData = linesLayout.getLinesViewportData(50, 160); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [50]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50]); // viewport 51->161 viewportData = linesLayout.getLinesViewportData(51, 161); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [50, 160]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50, 160]); // viewport 150->169 viewportData = linesLayout.getLinesViewportData(150, 169); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 159->169 viewportData = linesLayout.getLinesViewportData(159, 169); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 160->169 viewportData = linesLayout.getLinesViewportData(160, 169); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 160->1000 viewportData = linesLayout.getLinesViewportData(160, 1000); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 10); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 10); - assert.deepEqual(viewportData.relativeVerticalOffset, [160, 170, 180, 190]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 10); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 10); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160, 170, 180, 190]); }); test('LinesLayout getLinesViewportData 2 & getWhitespaceViewportData', () => { @@ -601,27 +601,27 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(6,100), b(7, 50) - assert.equal(linesLayout.getLinesTotalHeight(), 250); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 30); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 50); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 160); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 220); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 230); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 240); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 250); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 30); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 50); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 160); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 220); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 230); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 240); // viewport 50->160 let viewportData = linesLayout.getLinesViewportData(50, 160); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [50]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50]); let whitespaceData = linesLayout.getWhitespaceViewportData(50, 160); - assert.deepEqual(whitespaceData, [{ + assert.deepStrictEqual(whitespaceData, [{ id: a, afterLineNumber: 6, verticalOffset: 60, @@ -630,13 +630,13 @@ suite('Editor ViewLayout - LinesLayout', () => { // viewport 50->219 viewportData = linesLayout.getLinesViewportData(50, 219); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [50, 160]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50, 160]); whitespaceData = linesLayout.getWhitespaceViewportData(50, 219); - assert.deepEqual(whitespaceData, [{ + assert.deepStrictEqual(whitespaceData, [{ id: a, afterLineNumber: 6, verticalOffset: 60, @@ -650,19 +650,19 @@ suite('Editor ViewLayout - LinesLayout', () => { // viewport 50->220 viewportData = linesLayout.getLinesViewportData(50, 220); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [50, 160]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50, 160]); // viewport 50->250 viewportData = linesLayout.getLinesViewportData(50, 250); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 10); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 10); - assert.deepEqual(viewportData.relativeVerticalOffset, [50, 160, 220, 230, 240]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 10); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 10); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50, 160, 220, 230, 240]); }); test('LinesLayout getWhitespaceAtVerticalOffset', () => { @@ -671,40 +671,40 @@ suite('Editor ViewLayout - LinesLayout', () => { let b = insertWhitespace(linesLayout, 7, 0, 50, 0); let whitespace = linesLayout.getWhitespaceAtVerticalOffset(0); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(59); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(60); - assert.equal(whitespace!.id, a); + assert.strictEqual(whitespace!.id, a); whitespace = linesLayout.getWhitespaceAtVerticalOffset(61); - assert.equal(whitespace!.id, a); + assert.strictEqual(whitespace!.id, a); whitespace = linesLayout.getWhitespaceAtVerticalOffset(159); - assert.equal(whitespace!.id, a); + assert.strictEqual(whitespace!.id, a); whitespace = linesLayout.getWhitespaceAtVerticalOffset(160); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(161); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(169); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(170); - assert.equal(whitespace!.id, b); + assert.strictEqual(whitespace!.id, b); whitespace = linesLayout.getWhitespaceAtVerticalOffset(171); - assert.equal(whitespace!.id, b); + assert.strictEqual(whitespace!.id, b); whitespace = linesLayout.getWhitespaceAtVerticalOffset(219); - assert.equal(whitespace!.id, b); + assert.strictEqual(whitespace!.id, b); whitespace = linesLayout.getWhitespaceAtVerticalOffset(220); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); }); test('LinesLayout', () => { @@ -714,230 +714,230 @@ suite('Editor ViewLayout - LinesLayout', () => { // Insert a whitespace after line number 2, of height 10 const a = insertWhitespace(linesLayout, 2, 0, 10, 0); // whitespaces: a(2, 10) - assert.equal(linesLayout.getWhitespacesCount(), 1); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 10); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 10); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 10); + assert.strictEqual(linesLayout.getWhitespacesCount(), 1); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 10); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 10); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 10); // Insert a whitespace again after line number 2, of height 20 let b = insertWhitespace(linesLayout, 2, 0, 20, 0); // whitespaces: a(2, 10), b(2, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 30); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 30); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); // Change last inserted whitespace height to 30 changeOneWhitespace(linesLayout, b, 2, 30); // whitespaces: a(2, 10), b(2, 30) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 40); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 40); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 40); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 40); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 40); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 40); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 40); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 40); // Remove last inserted whitespace removeWhitespace(linesLayout, b); // whitespaces: a(2, 10) - assert.equal(linesLayout.getWhitespacesCount(), 1); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 10); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 10); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 10); + assert.strictEqual(linesLayout.getWhitespacesCount(), 1); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 10); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 10); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 10); // Add a whitespace before the first line of height 50 b = insertWhitespace(linesLayout, 0, 0, 50, 0); // whitespaces: b(0, 50), a(2, 10) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 60); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 60); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 60); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 60); // Add a whitespace after line 4 of height 20 insertWhitespace(linesLayout, 4, 0, 20, 0); // whitespaces: b(0, 50), a(2, 10), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 3); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 10); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 60); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 80); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 80); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 80); + assert.strictEqual(linesLayout.getWhitespacesCount(), 3); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 10); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(2), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 60); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(2), 80); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 80); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 80); // Add a whitespace after line 3 of height 30 insertWhitespace(linesLayout, 3, 0, 30, 0); // whitespaces: b(0, 50), a(2, 10), d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 4); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 10); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(3), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(3), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 60); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 90); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(3), 110); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 110); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 90); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 110); + assert.strictEqual(linesLayout.getWhitespacesCount(), 4); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 10); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(2), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(3), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(3), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 60); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(2), 90); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(3), 110); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 110); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 90); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 110); // Change whitespace after line 2 to height of 100 changeOneWhitespace(linesLayout, a, 2, 100); // whitespaces: b(0, 50), a(2, 100), d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 4); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 100); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(3), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(3), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 150); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 180); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(3), 200); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 200); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 150); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 180); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 200); + assert.strictEqual(linesLayout.getWhitespacesCount(), 4); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 100); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(2), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(3), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(3), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 150); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(2), 180); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(3), 200); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 200); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 150); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 180); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 200); // Remove whitespace after line 2 removeWhitespace(linesLayout, a); // whitespaces: b(0, 50), d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 3); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 80); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 100); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 100); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 80); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 100); + assert.strictEqual(linesLayout.getWhitespacesCount(), 3); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(2), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 80); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(2), 100); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 100); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 80); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 100); // Remove whitespace before line 1 removeWhitespace(linesLayout, b); // whitespaces: d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); // Delete line 1 linesLayout.onLinesDeleted(1, 1); // whitespaces: d(2, 30), c(3, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); // Insert a line before line 1 linesLayout.onLinesInserted(1, 1); // whitespaces: d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); // Delete line 4 linesLayout.onLinesDeleted(4, 4); // whitespaces: d(3, 30), c(3, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); }); test('LinesLayout findInsertionIndex', () => { @@ -949,114 +949,114 @@ suite('Editor ViewLayout - LinesLayout', () => { let arr: EditorWhitespace[]; arr = makeInternalWhitespace([]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 0); arr = makeInternalWhitespace([1]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); arr = makeInternalWhitespace([1, 3]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); arr = makeInternalWhitespace([1, 3, 5]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); arr = makeInternalWhitespace([1, 3, 5], 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); arr = makeInternalWhitespace([1, 3, 5, 7]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); arr = makeInternalWhitespace([1, 3, 5, 7, 9]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 10, 0), 5); arr = makeInternalWhitespace([1, 3, 5, 7, 9, 11]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 11, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 12, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 11, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 12, 0), 6); arr = makeInternalWhitespace([1, 3, 5, 7, 9, 11, 13]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 11, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 12, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 13, 0), 7); - assert.equal(LinesLayout.findInsertionIndex(arr, 14, 0), 7); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 11, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 12, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 13, 0), 7); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 14, 0), 7); arr = makeInternalWhitespace([1, 3, 5, 7, 9, 11, 13, 15]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 11, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 12, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 13, 0), 7); - assert.equal(LinesLayout.findInsertionIndex(arr, 14, 0), 7); - assert.equal(LinesLayout.findInsertionIndex(arr, 15, 0), 8); - assert.equal(LinesLayout.findInsertionIndex(arr, 16, 0), 8); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 11, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 12, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 13, 0), 7); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 14, 0), 7); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 15, 0), 8); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 16, 0), 8); }); test('LinesLayout changeWhitespaceAfterLineNumber & getFirstWhitespaceIndexAfterLineNumber', () => { @@ -1066,121 +1066,121 @@ suite('Editor ViewLayout - LinesLayout', () => { const b = insertWhitespace(linesLayout, 7, 0, 1, 0); const c = insertWhitespace(linesLayout, 3, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Do not really move a changeOneWhitespace(linesLayout, a, 1, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 1 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 1 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 1); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Do not really move a changeOneWhitespace(linesLayout, a, 2, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 2 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 2 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Change a to conflict with c => a gets placed after c changeOneWhitespace(linesLayout, a, 3, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), a); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), a); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Make a no-op changeOneWhitespace(linesLayout, c, 3, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), a); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), a); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Conflict c with b => c gets placed after b changeOneWhitespace(linesLayout, c, 7, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 7); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 1); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 1); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 1); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 1); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 1); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 1); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 1); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 1); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- }); test('LinesLayout Bug', () => { @@ -1189,53 +1189,53 @@ suite('Editor ViewLayout - LinesLayout', () => { const a = insertWhitespace(linesLayout, 0, 0, 1, 0); const b = insertWhitespace(linesLayout, 7, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), b); // 7 const c = insertWhitespace(linesLayout, 3, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 const d = insertWhitespace(linesLayout, 2, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), b); // 7 const e = insertWhitespace(linesLayout, 8, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 - assert.equal(linesLayout.getIdForWhitespaceIndex(4), e); // 8 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(4), e); // 8 const f = insertWhitespace(linesLayout, 11, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 - assert.equal(linesLayout.getIdForWhitespaceIndex(4), e); // 8 - assert.equal(linesLayout.getIdForWhitespaceIndex(5), f); // 11 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(4), e); // 8 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(5), f); // 11 const g = insertWhitespace(linesLayout, 10, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 - assert.equal(linesLayout.getIdForWhitespaceIndex(4), e); // 8 - assert.equal(linesLayout.getIdForWhitespaceIndex(5), g); // 10 - assert.equal(linesLayout.getIdForWhitespaceIndex(6), f); // 11 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(4), e); // 8 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(5), g); // 10 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(6), f); // 11 const h = insertWhitespace(linesLayout, 0, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), h); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(4), b); // 7 - assert.equal(linesLayout.getIdForWhitespaceIndex(5), e); // 8 - assert.equal(linesLayout.getIdForWhitespaceIndex(6), g); // 10 - assert.equal(linesLayout.getIdForWhitespaceIndex(7), f); // 11 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), h); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(4), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(5), e); // 8 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(6), g); // 10 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(7), f); // 11 }); }); diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 46f369013..672a4a86f 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -48,7 +48,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expected + ''); + assert.strictEqual(_actual.html, '' + expected + ''); assertCharacterMapping(_actual.characterMapping, expectedCharOffsetInPart, expectedPartLengts); } @@ -101,7 +101,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expected + ''); + assert.strictEqual(_actual.html, '' + expected + ''); assertCharacterMapping(_actual.characterMapping, expectedCharOffsetInPart, expectedPartLengts); } @@ -167,7 +167,7 @@ suite('viewLineRenderer.renderLine', () => { '' ].join(''); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); assertCharacterMapping(_actual.characterMapping, [ [0], @@ -252,7 +252,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [4, 4, 6, 1, 5, 1, 4, 1, 1, 1, 3, 15, 2, 3]); }); @@ -318,7 +318,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [12, 12, 24, 1, 21, 2, 1, 20, 1, 1]); }); @@ -384,7 +384,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [12, 12, 24, 1, 21, 2, 1, 20, 1, 1]); }); @@ -444,7 +444,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(actual.html, '' + expectedOutput + ''); + assert.strictEqual(actual.html, '' + expectedOutput + ''); assertCharacterMapping2(actual.characterMapping, expectedCharacterMapping); }); @@ -487,8 +487,8 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); - assert.equal(_actual.containsRTL, true); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.containsRTL, true); }); test('issue #6885: Splits large tokens', () => { @@ -520,7 +520,7 @@ suite('viewLineRenderer.renderLine', () => { false, null )); - assert.equal(actual.html, '' + expectedOutput.join('') + '', message); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + '', message); } // A token with 49 chars @@ -624,7 +624,7 @@ suite('viewLineRenderer.renderLine', () => { true, null )); - assert.equal(actual.html, '' + expectedOutput.join('') + '', message); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + '', message); } // A token with 101 chars @@ -669,7 +669,7 @@ suite('viewLineRenderer.renderLine', () => { let expectedOutput = [ 'a𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷', ]; - assert.equal(actual.html, '' + expectedOutput.join('') + ''); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + ''); }); test('issue #6885: Does not split large tokens in RTL text', () => { @@ -699,8 +699,8 @@ suite('viewLineRenderer.renderLine', () => { false, null )); - assert.equal(actual.html, '' + expectedOutput.join('') + ''); - assert.equal(actual.containsRTL, true); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + ''); + assert.strictEqual(actual.containsRTL, true); }); test('issue #95685: Uses unicode replacement character for Paragraph Separator', () => { @@ -730,7 +730,7 @@ suite('viewLineRenderer.renderLine', () => { false, null )); - assert.equal(actual.html, '' + expectedOutput.join('') + ''); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + ''); }); test('issue #19673: Monokai Theme bad-highlighting in line wrap', () => { @@ -780,7 +780,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); }); interface ICharMappingData { @@ -807,7 +807,7 @@ suite('viewLineRenderer.renderLine', () => { function assertCharacterMapping2(actual: CharacterMapping, expected: CharacterMapping): void { const _actual = decodeCharacterMapping(actual); const _expected = decodeCharacterMapping(expected); - assert.deepEqual(_actual, _expected); + assert.deepStrictEqual(_actual, _expected); } function assertCharacterMapping(actual: CharacterMapping, expectedCharPartOffsets: number[][], expectedPartLengths: number[]): void { @@ -830,7 +830,7 @@ suite('viewLineRenderer.renderLine', () => { for (let i = 0; i < tmp.length; i++) { actualCharOffset[i] = tmp[i]; } - assert.deepEqual(actualCharOffset, expectedCharAbsoluteOffset); + assert.deepStrictEqual(actualCharOffset, expectedCharAbsoluteOffset); } function assertCharPartOffsets(actual: CharacterMapping, expected: number[][]): void { @@ -844,7 +844,7 @@ suite('viewLineRenderer.renderLine', () => { let actualPartIndex = CharacterMapping.getPartIndex(_actualPartData); let actualCharIndex = CharacterMapping.getCharIndex(_actualPartData); - assert.deepEqual( + assert.deepStrictEqual( { partIndex: actualPartIndex, charIndex: actualCharIndex }, { partIndex: partIndex, charIndex: charIndex }, `character mapping for offset ${charOffset}` @@ -853,7 +853,7 @@ suite('viewLineRenderer.renderLine', () => { // here let actualOffset = actual.partDataToCharOffset(partIndex, part[part.length - 1] + 1, charIndex); - assert.equal( + assert.strictEqual( actualOffset, charOffset, `character mapping for part ${partIndex}, ${charIndex}` @@ -863,7 +863,7 @@ suite('viewLineRenderer.renderLine', () => { } } - assert.equal(actual.length, charOffset); + assert.strictEqual(actual.length, charOffset); } }); @@ -892,7 +892,7 @@ suite('viewLineRenderer.renderLine 2', () => { selections )); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); } test('issue #18616: Inline decorations ending at the text length are no longer rendered', () => { @@ -927,7 +927,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #19207: Link in Monokai is not rendered correctly', () => { @@ -976,7 +976,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('createLineParts simple', () => { @@ -1477,7 +1477,7 @@ suite('viewLineRenderer.renderLine 2', () => { // bb--------- // -cccccc---- - assert.deepEqual(actual.html, [ + assert.deepStrictEqual(actual.html, [ '', 'H', 'e', @@ -1522,7 +1522,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #32436: Non-monospace font + visible whitespace + After decorator causes line to "jump"', () => { @@ -1559,7 +1559,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #30133: Empty lines don\'t render inline decorations', () => { @@ -1594,7 +1594,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #37208: Collapsing bullet point containing emoji in Markdown document results in [??] character', () => { @@ -1628,7 +1628,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #37401 #40127: Allow both before and after decorations on empty line', () => { @@ -1665,7 +1665,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #38935: GitLens end-of-line blame no longer rendering', () => { @@ -1702,7 +1702,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #22832: Consider fullwidth characters when rendering tabs', () => { @@ -1735,7 +1735,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #22832: Consider fullwidth characters when rendering tabs (render whitespace)', () => { @@ -1774,7 +1774,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #22352: COMBINING ACUTE ACCENT (U+0301)', () => { @@ -1807,7 +1807,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #22352: Partially Broken Complex Script Rendering of Tamil', () => { @@ -1842,7 +1842,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #42700: Hindi characters are not being rendered properly', () => { @@ -1877,7 +1877,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #38123: editor.renderWhitespace: "boundary" renders whitespace at line wrap point when line is wrapped', () => { @@ -1909,7 +1909,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #33525: Long line with ligatures takes a long time to paint decorations', () => { @@ -1945,7 +1945,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #33525: Long line with ligatures takes a long time to paint decorations - not possible', () => { @@ -1977,7 +1977,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #91936: Semantic token color highlighting fails on line with selected text', () => { @@ -2062,7 +2062,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); @@ -2092,7 +2092,7 @@ suite('viewLineRenderer.renderLine 2', () => { return (partIndex: number, partLength: number, offset: number, expected: number) => { let charOffset = renderLineOutput.characterMapping.partDataToCharOffset(partIndex, partLength, offset); let actual = charOffset + 1; - assert.equal(actual, expected, 'getColumnOfLinePartOffset for ' + partIndex + ' @ ' + offset); + assert.strictEqual(actual, expected, 'getColumnOfLinePartOffset for ' + partIndex + ' @ ' + offset); }; } diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index 670f42407..eec0e2b27 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -47,6 +47,7 @@ function toAnnotatedText(text: string, lineBreakData: LineBreakData | null): str function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, text: string, previousLineBreakData: LineBreakData | null): LineBreakData | null { const fontInfo = new FontInfo({ zoomLevel: 0, + pixelRatio: 1, fontFamily: 'testFontFamily', fontWeight: 'normal', fontSize: 14, @@ -55,7 +56,7 @@ function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, letterSpacing: 0, isMonospace: true, typicalHalfwidthCharacterWidth: 7, - typicalFullwidthCharacterWidth: 14, + typicalFullwidthCharacterWidth: 7 * columnsForFullWidthChar, canUseHalfwidthRightwardsArrow: true, spaceWidth: 7, middotWidth: 7, @@ -74,7 +75,7 @@ function assertLineBreaks(factory: ILineBreaksComputerFactory, tabSize: number, const lineBreakData = getLineBreakData(factory, tabSize, breakAfter, 2, wrappingIndent, text, null); const actualAnnotatedText = toAnnotatedText(text, lineBreakData); - assert.equal(actualAnnotatedText, annotatedText); + assert.strictEqual(actualAnnotatedText, annotatedText); return lineBreakData; } @@ -122,28 +123,41 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { assertLineBreaks(factory, 4, 5, 'aa.(.|).aaa'); }); - function assertIncrementalLineBreaks(factory: ILineBreaksComputerFactory, text: string, tabSize: number, breakAfter1: number, annotatedText1: string, breakAfter2: number, annotatedText2: string, wrappingIndent = WrappingIndent.None): void { + function assertLineBreakDataEqual(a: LineBreakData | null, b: LineBreakData | null): void { + if (!a || !b) { + assert.deepStrictEqual(a, b); + return; + } + assert.deepStrictEqual(a.breakOffsets, b.breakOffsets); + assert.deepStrictEqual(a.wrappedTextIndentLength, b.wrappedTextIndentLength); + for (let i = 0; i < a.breakOffsetsVisibleColumn.length; i++) { + const diff = a.breakOffsetsVisibleColumn[i] - b.breakOffsetsVisibleColumn[i]; + assert.ok(diff < 0.001); + } + } + + function assertIncrementalLineBreaks(factory: ILineBreaksComputerFactory, text: string, tabSize: number, breakAfter1: number, annotatedText1: string, breakAfter2: number, annotatedText2: string, wrappingIndent = WrappingIndent.None, columnsForFullWidthChar: number = 2): void { // sanity check the test - assert.equal(text, parseAnnotatedText(annotatedText1).text); - assert.equal(text, parseAnnotatedText(annotatedText2).text); + assert.strictEqual(text, parseAnnotatedText(annotatedText1).text); + assert.strictEqual(text, parseAnnotatedText(annotatedText2).text); // check that the direct mapping is ok for 1 - const directLineBreakData1 = getLineBreakData(factory, tabSize, breakAfter1, 2, wrappingIndent, text, null); - assert.equal(toAnnotatedText(text, directLineBreakData1), annotatedText1); + const directLineBreakData1 = getLineBreakData(factory, tabSize, breakAfter1, columnsForFullWidthChar, wrappingIndent, text, null); + assert.strictEqual(toAnnotatedText(text, directLineBreakData1), annotatedText1); // check that the direct mapping is ok for 2 - const directLineBreakData2 = getLineBreakData(factory, tabSize, breakAfter2, 2, wrappingIndent, text, null); - assert.equal(toAnnotatedText(text, directLineBreakData2), annotatedText2); + const directLineBreakData2 = getLineBreakData(factory, tabSize, breakAfter2, columnsForFullWidthChar, wrappingIndent, text, null); + assert.strictEqual(toAnnotatedText(text, directLineBreakData2), annotatedText2); // check that going from 1 to 2 is ok - const lineBreakData2from1 = getLineBreakData(factory, tabSize, breakAfter2, 2, wrappingIndent, text, directLineBreakData1); - assert.equal(toAnnotatedText(text, lineBreakData2from1), annotatedText2); - assert.deepEqual(lineBreakData2from1, directLineBreakData2); + const lineBreakData2from1 = getLineBreakData(factory, tabSize, breakAfter2, columnsForFullWidthChar, wrappingIndent, text, directLineBreakData1); + assert.strictEqual(toAnnotatedText(text, lineBreakData2from1), annotatedText2); + assertLineBreakDataEqual(lineBreakData2from1, directLineBreakData2); // check that going from 2 to 1 is ok - const lineBreakData1from2 = getLineBreakData(factory, tabSize, breakAfter1, 2, wrappingIndent, text, directLineBreakData2); - assert.equal(toAnnotatedText(text, lineBreakData1from2), annotatedText1); - assert.deepEqual(lineBreakData1from2, directLineBreakData1); + const lineBreakData1from2 = getLineBreakData(factory, tabSize, breakAfter1, columnsForFullWidthChar, wrappingIndent, text, directLineBreakData2); + assert.strictEqual(toAnnotatedText(text, lineBreakData1from2), annotatedText1); + assertLineBreakDataEqual(lineBreakData1from2, directLineBreakData1); } test('MonospaceLineBreaksComputer incremental 1', () => { @@ -216,6 +230,19 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { ); }); + test('issue #110392: Occasional crash when resize with panel on the right', () => { + const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + assertIncrementalLineBreaks( + factory, + '你好 **hello** **hello** **hello-world** hey there!', + 4, + 15, '你好 **hello** |**hello** |**hello-world**| hey there!', + 1, '你|好| |*|*|h|e|l|l|o|*|*| |*|*|h|e|l|l|o|*|*| |*|*|h|e|l|l|o|-|w|o|r|l|d|*|*| |h|e|y| |t|h|e|r|e|!', + WrappingIndent.Same, + 1.6605405405405405 + ); + }); + test('MonospaceLineBreaksComputer - CJK and Kinsoku Shori', () => { let factory = new MonospaceLineBreaksComputerFactory('(', '\t)'); assertLineBreaks(factory, 4, 5, 'aa \u5b89|\u5b89'); @@ -239,7 +266,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { test('issue #35162: wrappingIndent not consistently working', () => { let factory = new MonospaceLineBreaksComputerFactory('', '\t '); let mapper = assertLineBreaks(factory, 4, 24, ' t h i s |i s |a l |o n |g l |i n |e', WrappingIndent.Indent); - assert.equal(mapper!.wrappedTextIndentLength, ' '.length); + assert.strictEqual(mapper!.wrappedTextIndentLength, ' '.length); }); test('issue #75494: surrogate pairs', () => { @@ -260,11 +287,16 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { test('MonospaceLineBreaksComputer - WrappingIndent.DeepIndent', () => { let factory = new MonospaceLineBreaksComputerFactory('', '\t '); let mapper = assertLineBreaks(factory, 4, 26, ' W e A r e T e s t |i n g D e |e p I n d |e n t a t |i o n', WrappingIndent.DeepIndent); - assert.equal(mapper!.wrappedTextIndentLength, ' '.length); + assert.strictEqual(mapper!.wrappedTextIndentLength, ' '.length); }); test('issue #33366: Word wrap algorithm behaves differently around punctuation', () => { const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); assertLineBreaks(factory, 4, 23, 'this is a line of |text, text that sits |on a line', WrappingIndent.Same); }); + + test('issue #112382: Word wrap doesn\'t work well with control characters', () => { + const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + assertLineBreaks(factory, 4, 6, '\x06\x06\x06|\x06\x06\x06', WrappingIndent.Same); + }); }); diff --git a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts index 65cc26efd..80df1a94e 100644 --- a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts @@ -22,158 +22,158 @@ suite('Editor ViewModel - PrefixSumComputer', () => { let indexOfResult: PrefixSumIndexOfResult; let psc = new PrefixSumComputer(toUint32Array([1, 1, 2, 1, 3])); - assert.equal(psc.getTotalValue(), 8); - assert.equal(psc.getAccumulatedValue(-1), 0); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 2); - assert.equal(psc.getAccumulatedValue(2), 4); - assert.equal(psc.getAccumulatedValue(3), 5); - assert.equal(psc.getAccumulatedValue(4), 8); + assert.strictEqual(psc.getTotalValue(), 8); + assert.strictEqual(psc.getAccumulatedValue(-1), 0); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 2); + assert.strictEqual(psc.getAccumulatedValue(2), 4); + assert.strictEqual(psc.getAccumulatedValue(3), 5); + assert.strictEqual(psc.getAccumulatedValue(4), 8); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 1); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 1); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 2); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 2); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 2); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 2); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 3); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 3); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(5); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(6); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(7); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 2); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 2); indexOfResult = psc.getIndexOf(8); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 3); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 3); // [1, 2, 2, 1, 3] psc.changeValue(1, 2); - assert.equal(psc.getTotalValue(), 9); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 3); - assert.equal(psc.getAccumulatedValue(2), 5); - assert.equal(psc.getAccumulatedValue(3), 6); - assert.equal(psc.getAccumulatedValue(4), 9); + assert.strictEqual(psc.getTotalValue(), 9); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 3); + assert.strictEqual(psc.getAccumulatedValue(2), 5); + assert.strictEqual(psc.getAccumulatedValue(3), 6); + assert.strictEqual(psc.getAccumulatedValue(4), 9); // [1, 0, 2, 1, 3] psc.changeValue(1, 0); - assert.equal(psc.getTotalValue(), 7); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 1); - assert.equal(psc.getAccumulatedValue(2), 3); - assert.equal(psc.getAccumulatedValue(3), 4); - assert.equal(psc.getAccumulatedValue(4), 7); + assert.strictEqual(psc.getTotalValue(), 7); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 1); + assert.strictEqual(psc.getAccumulatedValue(2), 3); + assert.strictEqual(psc.getAccumulatedValue(3), 4); + assert.strictEqual(psc.getAccumulatedValue(4), 7); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 2); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 2); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 2); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 2); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 3); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 3); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(5); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(6); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 2); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 2); indexOfResult = psc.getIndexOf(7); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 3); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 3); // [1, 0, 0, 1, 3] psc.changeValue(2, 0); - assert.equal(psc.getTotalValue(), 5); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 1); - assert.equal(psc.getAccumulatedValue(2), 1); - assert.equal(psc.getAccumulatedValue(3), 2); - assert.equal(psc.getAccumulatedValue(4), 5); + assert.strictEqual(psc.getTotalValue(), 5); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 1); + assert.strictEqual(psc.getAccumulatedValue(2), 1); + assert.strictEqual(psc.getAccumulatedValue(3), 2); + assert.strictEqual(psc.getAccumulatedValue(4), 5); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 3); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 3); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 2); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 2); indexOfResult = psc.getIndexOf(5); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 3); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 3); // [1, 0, 0, 0, 3] psc.changeValue(3, 0); - assert.equal(psc.getTotalValue(), 4); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 1); - assert.equal(psc.getAccumulatedValue(2), 1); - assert.equal(psc.getAccumulatedValue(3), 1); - assert.equal(psc.getAccumulatedValue(4), 4); + assert.strictEqual(psc.getTotalValue(), 4); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 1); + assert.strictEqual(psc.getAccumulatedValue(2), 1); + assert.strictEqual(psc.getAccumulatedValue(3), 1); + assert.strictEqual(psc.getAccumulatedValue(4), 4); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 2); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 2); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 3); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 3); // [1, 1, 0, 1, 1] psc.changeValue(1, 1); psc.changeValue(3, 1); psc.changeValue(4, 1); - assert.equal(psc.getTotalValue(), 4); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 2); - assert.equal(psc.getAccumulatedValue(2), 2); - assert.equal(psc.getAccumulatedValue(3), 3); - assert.equal(psc.getAccumulatedValue(4), 4); + assert.strictEqual(psc.getTotalValue(), 4); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 2); + assert.strictEqual(psc.getAccumulatedValue(2), 2); + assert.strictEqual(psc.getAccumulatedValue(3), 3); + assert.strictEqual(psc.getAccumulatedValue(4), 4); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 1); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 1); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 3); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 3); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); }); }); diff --git a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts index 3156be0e1..3f7978952 100644 --- a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts +++ b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts @@ -25,42 +25,42 @@ suite('Editor ViewModel - SplitLinesCollection', () => { let model1 = createModel('My First LineMy Second LineAnd another one'); let line1 = createSplitLine([13, 14, 15], [13, 13 + 14, 13 + 14 + 15], 0); - assert.equal(line1.getViewLineCount(), 3); - assert.equal(line1.getViewLineContent(model1, 1, 0), 'My First Line'); - assert.equal(line1.getViewLineContent(model1, 1, 1), 'My Second Line'); - assert.equal(line1.getViewLineContent(model1, 1, 2), 'And another one'); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 0), 14); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 1), 15); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 2), 16); + assert.strictEqual(line1.getViewLineCount(), 3); + assert.strictEqual(line1.getViewLineContent(model1, 1, 0), 'My First Line'); + assert.strictEqual(line1.getViewLineContent(model1, 1, 1), 'My Second Line'); + assert.strictEqual(line1.getViewLineContent(model1, 1, 2), 'And another one'); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 0), 14); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 1), 15); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 2), 16); for (let col = 1; col <= 14; col++) { - assert.equal(line1.getModelColumnOfViewPosition(0, col), col, 'getInputColumnOfOutputPosition(0, ' + col + ')'); + assert.strictEqual(line1.getModelColumnOfViewPosition(0, col), col, 'getInputColumnOfOutputPosition(0, ' + col + ')'); } for (let col = 1; col <= 15; col++) { - assert.equal(line1.getModelColumnOfViewPosition(1, col), 13 + col, 'getInputColumnOfOutputPosition(1, ' + col + ')'); + assert.strictEqual(line1.getModelColumnOfViewPosition(1, col), 13 + col, 'getInputColumnOfOutputPosition(1, ' + col + ')'); } for (let col = 1; col <= 16; col++) { - assert.equal(line1.getModelColumnOfViewPosition(2, col), 13 + 14 + col, 'getInputColumnOfOutputPosition(2, ' + col + ')'); + assert.strictEqual(line1.getModelColumnOfViewPosition(2, col), 13 + 14 + col, 'getInputColumnOfOutputPosition(2, ' + col + ')'); } for (let col = 1; col <= 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), 'getOutputPositionOfInputPosition(' + col + ')'); } for (let col = 1 + 13; col <= 14 + 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, col - 13), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, col - 13), 'getOutputPositionOfInputPosition(' + col + ')'); } for (let col = 1 + 13 + 14; col <= 15 + 14 + 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, col - 13 - 14), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, col - 13 - 14), 'getOutputPositionOfInputPosition(' + col + ')'); } model1 = createModel('My First LineMy Second LineAnd another one'); line1 = createSplitLine([13, 14, 15], [13, 13 + 14, 13 + 14 + 15], 4); - assert.equal(line1.getViewLineCount(), 3); - assert.equal(line1.getViewLineContent(model1, 1, 0), 'My First Line'); - assert.equal(line1.getViewLineContent(model1, 1, 1), ' My Second Line'); - assert.equal(line1.getViewLineContent(model1, 1, 2), ' And another one'); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 0), 14); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 1), 19); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 2), 20); + assert.strictEqual(line1.getViewLineCount(), 3); + assert.strictEqual(line1.getViewLineContent(model1, 1, 0), 'My First Line'); + assert.strictEqual(line1.getViewLineContent(model1, 1, 1), ' My Second Line'); + assert.strictEqual(line1.getViewLineContent(model1, 1, 2), ' And another one'); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 0), 14); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 1), 19); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 2), 20); let actualViewColumnMapping: number[][] = []; for (let lineIndex = 0; lineIndex < line1.getViewLineCount(); lineIndex++) { @@ -70,20 +70,20 @@ suite('Editor ViewModel - SplitLinesCollection', () => { } actualViewColumnMapping.push(actualLineViewColumnMapping); } - assert.deepEqual(actualViewColumnMapping, [ + assert.deepStrictEqual(actualViewColumnMapping, [ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], [14, 14, 14, 14, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], [28, 28, 28, 28, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43], ]); for (let col = 1; col <= 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), '6.getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), '6.getOutputPositionOfInputPosition(' + col + ')'); } for (let col = 1 + 13; col <= 14 + 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, 4 + col - 13), '7.getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, 4 + col - 13), '7.getOutputPositionOfInputPosition(' + col + ')'); } for (let col = 1 + 13 + 14; col <= 15 + 14 + 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, 4 + col - 13 - 14), '8.getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, 4 + col - 13 - 14), '8.getOutputPositionOfInputPosition(' + col + ')'); } }); @@ -136,65 +136,65 @@ suite('Editor ViewModel - SplitLinesCollection', () => { ].join('\n'); withSplitLinesCollection(text, (model, linesCollection) => { - assert.equal(linesCollection.getViewLineCount(), 6); + assert.strictEqual(linesCollection.getViewLineCount(), 6); // getOutputIndentGuide - assert.deepEqual(linesCollection.getViewLinesIndentGuides(-1, -1), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(0, 0), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(1, 1), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(2, 2), [1]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(3, 3), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(4, 4), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(5, 5), [1]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(6, 6), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(7, 7), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(-1, -1), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(0, 0), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(1, 1), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(2, 2), [1]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(3, 3), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(4, 4), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(5, 5), [1]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(6, 6), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(7, 7), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(0, 7), [0, 1, 0, 0, 1, 0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(0, 7), [0, 1, 0, 0, 1, 0]); // getOutputLineContent - assert.equal(linesCollection.getViewLineContent(-1), 'int main() {'); - assert.equal(linesCollection.getViewLineContent(0), 'int main() {'); - assert.equal(linesCollection.getViewLineContent(1), 'int main() {'); - assert.equal(linesCollection.getViewLineContent(2), '\tprintf("Hello world!");'); - assert.equal(linesCollection.getViewLineContent(3), '}'); - assert.equal(linesCollection.getViewLineContent(4), 'int main() {'); - assert.equal(linesCollection.getViewLineContent(5), '\tprintf("Hello world!");'); - assert.equal(linesCollection.getViewLineContent(6), '}'); - assert.equal(linesCollection.getViewLineContent(7), '}'); + assert.strictEqual(linesCollection.getViewLineContent(-1), 'int main() {'); + assert.strictEqual(linesCollection.getViewLineContent(0), 'int main() {'); + assert.strictEqual(linesCollection.getViewLineContent(1), 'int main() {'); + assert.strictEqual(linesCollection.getViewLineContent(2), '\tprintf("Hello world!");'); + assert.strictEqual(linesCollection.getViewLineContent(3), '}'); + assert.strictEqual(linesCollection.getViewLineContent(4), 'int main() {'); + assert.strictEqual(linesCollection.getViewLineContent(5), '\tprintf("Hello world!");'); + assert.strictEqual(linesCollection.getViewLineContent(6), '}'); + assert.strictEqual(linesCollection.getViewLineContent(7), '}'); // getOutputLineMinColumn - assert.equal(linesCollection.getViewLineMinColumn(-1), 1); - assert.equal(linesCollection.getViewLineMinColumn(0), 1); - assert.equal(linesCollection.getViewLineMinColumn(1), 1); - assert.equal(linesCollection.getViewLineMinColumn(2), 1); - assert.equal(linesCollection.getViewLineMinColumn(3), 1); - assert.equal(linesCollection.getViewLineMinColumn(4), 1); - assert.equal(linesCollection.getViewLineMinColumn(5), 1); - assert.equal(linesCollection.getViewLineMinColumn(6), 1); - assert.equal(linesCollection.getViewLineMinColumn(7), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(-1), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(0), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(1), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(2), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(3), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(4), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(5), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(6), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(7), 1); // getOutputLineMaxColumn - assert.equal(linesCollection.getViewLineMaxColumn(-1), 13); - assert.equal(linesCollection.getViewLineMaxColumn(0), 13); - assert.equal(linesCollection.getViewLineMaxColumn(1), 13); - assert.equal(linesCollection.getViewLineMaxColumn(2), 25); - assert.equal(linesCollection.getViewLineMaxColumn(3), 2); - assert.equal(linesCollection.getViewLineMaxColumn(4), 13); - assert.equal(linesCollection.getViewLineMaxColumn(5), 25); - assert.equal(linesCollection.getViewLineMaxColumn(6), 2); - assert.equal(linesCollection.getViewLineMaxColumn(7), 2); + assert.strictEqual(linesCollection.getViewLineMaxColumn(-1), 13); + assert.strictEqual(linesCollection.getViewLineMaxColumn(0), 13); + assert.strictEqual(linesCollection.getViewLineMaxColumn(1), 13); + assert.strictEqual(linesCollection.getViewLineMaxColumn(2), 25); + assert.strictEqual(linesCollection.getViewLineMaxColumn(3), 2); + assert.strictEqual(linesCollection.getViewLineMaxColumn(4), 13); + assert.strictEqual(linesCollection.getViewLineMaxColumn(5), 25); + assert.strictEqual(linesCollection.getViewLineMaxColumn(6), 2); + assert.strictEqual(linesCollection.getViewLineMaxColumn(7), 2); // convertOutputPositionToInputPosition - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(-1, 1), new Position(1, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(0, 1), new Position(1, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(1, 1), new Position(1, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(2, 1), new Position(2, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(3, 1), new Position(3, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(4, 1), new Position(4, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(5, 1), new Position(5, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(6, 1), new Position(6, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(7, 1), new Position(6, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(8, 1), new Position(6, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(-1, 1), new Position(1, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(0, 1), new Position(1, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(1, 1), new Position(1, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(2, 1), new Position(2, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(3, 1), new Position(3, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(4, 1), new Position(4, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(5, 1), new Position(5, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(6, 1), new Position(6, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(7, 1), new Position(6, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(8, 1), new Position(6, 1)); }); }); @@ -216,7 +216,7 @@ suite('Editor ViewModel - SplitLinesCollection', () => { ]); let viewLineCount = linesCollection.getViewLineCount(); - assert.equal(viewLineCount, 1, 'getOutputLineCount()'); + assert.strictEqual(viewLineCount, 1, 'getOutputLineCount()'); let modelLineCount = model.getLineCount(); for (let lineNumber = 0; lineNumber <= modelLineCount + 1; lineNumber++) { @@ -244,7 +244,7 @@ suite('Editor ViewModel - SplitLinesCollection', () => { viewColumn = viewMaxColumn; } let validViewPosition = new Position(viewLineNumber, viewColumn); - assert.equal(viewPosition.toString(), validViewPosition.toString(), 'model->view for ' + lineNumber + ', ' + column); + assert.strictEqual(viewPosition.toString(), validViewPosition.toString(), 'model->view for ' + lineNumber + ', ' + column); } } @@ -254,7 +254,7 @@ suite('Editor ViewModel - SplitLinesCollection', () => { for (let column = lineMinColumn - 1; column <= lineMaxColumn + 1; column++) { let modelPosition = linesCollection.convertViewPositionToModelPosition(lineNumber, column); let validModelPosition = model.validatePosition(modelPosition); - assert.equal(modelPosition.toString(), validModelPosition.toString(), 'view->model for ' + lineNumber + ', ' + column); + assert.strictEqual(modelPosition.toString(), validModelPosition.toString(), 'view->model for ' + lineNumber + ', ' + column); } } }); @@ -333,7 +333,7 @@ suite('SplitLinesCollection', () => { const tokenizationSupport: modes.ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: modes.IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 => { let tokens = _tokens[_lineIndex++]; let result = new Uint32Array(2 * tokens.length); @@ -374,7 +374,7 @@ suite('SplitLinesCollection', () => { value: _actual.getForeground(i) }; } - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } interface ITestMinimapLineRenderingData { @@ -391,16 +391,15 @@ suite('SplitLinesCollection', () => { } if (expected === null) { assert.ok(false); - return; } - assert.equal(actual.content, expected.content); - assert.equal(actual.minColumn, expected.minColumn); - assert.equal(actual.maxColumn, expected.maxColumn); + assert.strictEqual(actual.content, expected.content); + assert.strictEqual(actual.minColumn, expected.minColumn); + assert.strictEqual(actual.maxColumn, expected.maxColumn); assertViewLineTokens(actual.tokens, expected.tokens); } function assertMinimapLinesRenderingData(actual: ViewLineData[], expected: Array): void { - assert.equal(actual.length, expected.length); + assert.strictEqual(actual.length, expected.length); for (let i = 0; i < expected.length; i++) { assertMinimapLineRenderingData(actual[i], expected[i]); } @@ -429,15 +428,15 @@ suite('SplitLinesCollection', () => { test('getViewLinesData - no wrapping', () => { withSplitLinesCollection(model!, 'off', 0, (splitLinesCollection) => { - assert.equal(splitLinesCollection.getViewLineCount(), 8); - assert.equal(splitLinesCollection.modelPositionIsVisible(1, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(2, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(3, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(4, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(5, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(6, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(7, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(8, 1), true); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 8); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); let _expected: ITestMinimapLineRenderingData[] = [ { @@ -541,15 +540,15 @@ suite('SplitLinesCollection', () => { ]); splitLinesCollection.setHiddenAreas([new Range(2, 1, 4, 1)]); - assert.equal(splitLinesCollection.getViewLineCount(), 5); - assert.equal(splitLinesCollection.modelPositionIsVisible(1, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(2, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(3, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(4, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(5, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(6, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(7, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(8, 1), true); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 5); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); assertAllMinimapLinesRenderingData(splitLinesCollection, [ _expected[0], @@ -563,15 +562,15 @@ suite('SplitLinesCollection', () => { test('getViewLinesData - with wrapping', () => { withSplitLinesCollection(model!, 'wordWrapColumn', 30, (splitLinesCollection) => { - assert.equal(splitLinesCollection.getViewLineCount(), 12); - assert.equal(splitLinesCollection.modelPositionIsVisible(1, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(2, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(3, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(4, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(5, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(6, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(7, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(8, 1), true); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 12); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); let _expected: ITestMinimapLineRenderingData[] = [ { @@ -711,15 +710,15 @@ suite('SplitLinesCollection', () => { ]); splitLinesCollection.setHiddenAreas([new Range(2, 1, 4, 1)]); - assert.equal(splitLinesCollection.getViewLineCount(), 8); - assert.equal(splitLinesCollection.modelPositionIsVisible(1, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(2, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(3, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(4, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(5, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(6, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(7, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(8, 1), true); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 8); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); assertAllMinimapLinesRenderingData(splitLinesCollection, [ _expected[0], diff --git a/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts b/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts index cd9906811..9b2862773 100644 --- a/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; -import { InlineDecorationType } from 'vs/editor/common/viewModel/viewModel'; +import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewModel/viewModel'; import { testViewModel } from 'vs/editor/test/common/viewModel/testViewModel'; suite('ViewModelDecorations', () => { @@ -19,11 +19,11 @@ suite('ViewModelDecorations', () => { wordWrapColumn: 13 }; testViewModel(text, opts, (viewModel, model) => { - assert.equal(viewModel.getLineContent(1), 'hello world, '); - assert.equal(viewModel.getLineContent(2), 'this is a '); - assert.equal(viewModel.getLineContent(3), 'buffer that '); - assert.equal(viewModel.getLineContent(4), 'will be '); - assert.equal(viewModel.getLineContent(5), 'wrapped'); + assert.strictEqual(viewModel.getLineContent(1), 'hello world, '); + assert.strictEqual(viewModel.getLineContent(2), 'this is a '); + assert.strictEqual(viewModel.getLineContent(3), 'buffer that '); + assert.strictEqual(viewModel.getLineContent(4), 'will be '); + assert.strictEqual(viewModel.getLineContent(5), 'wrapped'); model.changeDecorations((accessor) => { let createOpts = (id: string) => { @@ -79,7 +79,7 @@ suite('ViewModelDecorations', () => { return dec.options.className; }).filter(Boolean); - assert.deepEqual(actualDecorations, [ + assert.deepStrictEqual(actualDecorations, [ 'dec1', 'dec2', 'dec3', @@ -102,112 +102,28 @@ suite('ViewModelDecorations', () => { ).inlineDecorations; // view line 2: (1,14 -> 1,24) - assert.deepEqual(inlineDecorations1, [ - { - range: new Range(1, 2, 2, 2), - inlineClassName: 'i-dec3', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 2, 2, 2), - inlineClassName: 'a-dec3', - type: InlineDecorationType.After - }, - { - range: new Range(1, 2, 3, 13), - inlineClassName: 'i-dec4', - type: InlineDecorationType.Regular - }, - { - range: new Range(1, 2, 5, 8), - inlineClassName: 'i-dec5', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'i-dec6', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'b-dec6', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'a-dec6', - type: InlineDecorationType.After - }, - { - range: new Range(2, 1, 2, 3), - inlineClassName: 'i-dec7', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'b-dec7', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 3, 2, 3), - inlineClassName: 'a-dec7', - type: InlineDecorationType.After - }, - { - range: new Range(2, 1, 3, 13), - inlineClassName: 'i-dec8', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'b-dec8', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 1, 5, 8), - inlineClassName: 'i-dec9', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'b-dec9', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 3, 2, 5), - inlineClassName: 'i-dec10', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 3, 2, 3), - inlineClassName: 'b-dec10', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 5, 2, 5), - inlineClassName: 'a-dec10', - type: InlineDecorationType.After - }, - { - range: new Range(2, 3, 3, 13), - inlineClassName: 'i-dec11', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 3, 2, 3), - inlineClassName: 'b-dec11', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 3, 5, 8), - inlineClassName: 'i-dec12', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 3, 2, 3), - inlineClassName: 'b-dec12', - type: InlineDecorationType.Before - }, + assert.deepStrictEqual(inlineDecorations1, [ + new InlineDecoration(new Range(1, 2, 2, 2), 'i-dec3', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 2, 2, 2), 'a-dec3', InlineDecorationType.After), + new InlineDecoration(new Range(1, 2, 3, 13), 'i-dec4', InlineDecorationType.Regular), + new InlineDecoration(new Range(1, 2, 5, 8), 'i-dec5', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'i-dec6', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'b-dec6', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 1, 2, 1), 'a-dec6', InlineDecorationType.After), + new InlineDecoration(new Range(2, 1, 2, 3), 'i-dec7', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'b-dec7', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 3, 2, 3), 'a-dec7', InlineDecorationType.After), + new InlineDecoration(new Range(2, 1, 3, 13), 'i-dec8', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'b-dec8', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 1, 5, 8), 'i-dec9', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'b-dec9', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 3, 2, 5), 'i-dec10', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 3, 2, 3), 'b-dec10', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 5, 2, 5), 'a-dec10', InlineDecorationType.After), + new InlineDecoration(new Range(2, 3, 3, 13), 'i-dec11', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 3, 2, 3), 'b-dec11', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 3, 5, 8), 'i-dec12', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 3, 2, 3), 'b-dec12', InlineDecorationType.Before), ]); let inlineDecorations2 = viewModel.getViewLineRenderingData( @@ -216,52 +132,16 @@ suite('ViewModelDecorations', () => { ).inlineDecorations; // view line 3 (24 -> 36) - assert.deepEqual(inlineDecorations2, [ - { - range: new Range(1, 2, 3, 13), - inlineClassName: 'i-dec4', - type: InlineDecorationType.Regular - }, - { - range: new Range(3, 13, 3, 13), - inlineClassName: 'a-dec4', - type: InlineDecorationType.After - }, - { - range: new Range(1, 2, 5, 8), - inlineClassName: 'i-dec5', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 3, 13), - inlineClassName: 'i-dec8', - type: InlineDecorationType.Regular - }, - { - range: new Range(3, 13, 3, 13), - inlineClassName: 'a-dec8', - type: InlineDecorationType.After - }, - { - range: new Range(2, 1, 5, 8), - inlineClassName: 'i-dec9', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 3, 3, 13), - inlineClassName: 'i-dec11', - type: InlineDecorationType.Regular - }, - { - range: new Range(3, 13, 3, 13), - inlineClassName: 'a-dec11', - type: InlineDecorationType.After - }, - { - range: new Range(2, 3, 5, 8), - inlineClassName: 'i-dec12', - type: InlineDecorationType.Regular - }, + assert.deepStrictEqual(inlineDecorations2, [ + new InlineDecoration(new Range(1, 2, 3, 13), 'i-dec4', InlineDecorationType.Regular), + new InlineDecoration(new Range(3, 13, 3, 13), 'a-dec4', InlineDecorationType.After), + new InlineDecoration(new Range(1, 2, 5, 8), 'i-dec5', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 3, 13), 'i-dec8', InlineDecorationType.Regular), + new InlineDecoration(new Range(3, 13, 3, 13), 'a-dec8', InlineDecorationType.After), + new InlineDecoration(new Range(2, 1, 5, 8), 'i-dec9', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 3, 3, 13), 'i-dec11', InlineDecorationType.Regular), + new InlineDecoration(new Range(3, 13, 3, 13), 'a-dec11', InlineDecorationType.After), + new InlineDecoration(new Range(2, 3, 5, 8), 'i-dec12', InlineDecorationType.Regular), ]); }); }); @@ -275,11 +155,11 @@ suite('ViewModelDecorations', () => { wordWrapColumn: 13 }; testViewModel(text, opts, (viewModel, model) => { - assert.equal(viewModel.getLineContent(1), 'hello world, '); - assert.equal(viewModel.getLineContent(2), 'this is a '); - assert.equal(viewModel.getLineContent(3), 'buffer that '); - assert.equal(viewModel.getLineContent(4), 'will be '); - assert.equal(viewModel.getLineContent(5), 'wrapped'); + assert.strictEqual(viewModel.getLineContent(1), 'hello world, '); + assert.strictEqual(viewModel.getLineContent(2), 'this is a '); + assert.strictEqual(viewModel.getLineContent(3), 'buffer that '); + assert.strictEqual(viewModel.getLineContent(4), 'will be '); + assert.strictEqual(viewModel.getLineContent(5), 'wrapped'); model.changeDecorations((accessor) => { accessor.addDecoration( @@ -293,19 +173,19 @@ suite('ViewModelDecorations', () => { let decorations = viewModel.getDecorationsInViewport( new Range(2, viewModel.getLineMinColumn(2), 3, viewModel.getLineMaxColumn(3)) ).filter(x => Boolean(x.options.beforeContentClassName)); - assert.deepEqual(decorations, []); + assert.deepStrictEqual(decorations, []); let inlineDecorations1 = viewModel.getViewLineRenderingData( new Range(2, viewModel.getLineMinColumn(2), 3, viewModel.getLineMaxColumn(3)), 2 ).inlineDecorations; - assert.deepEqual(inlineDecorations1, []); + assert.deepStrictEqual(inlineDecorations1, []); let inlineDecorations2 = viewModel.getViewLineRenderingData( new Range(2, viewModel.getLineMinColumn(2), 3, viewModel.getLineMaxColumn(3)), 3 ).inlineDecorations; - assert.deepEqual(inlineDecorations2, []); + assert.deepStrictEqual(inlineDecorations2, []); }); }); @@ -329,17 +209,9 @@ suite('ViewModelDecorations', () => { new Range(1, 1, 1, 1), 1 ).inlineDecorations; - assert.deepEqual(inlineDecorations, [ - { - range: new Range(1, 1, 1, 1), - inlineClassName: 'before1', - type: InlineDecorationType.Before - }, - { - range: new Range(1, 1, 1, 1), - inlineClassName: 'after1', - type: InlineDecorationType.After - } + assert.deepStrictEqual(inlineDecorations, [ + new InlineDecoration(new Range(1, 1, 1, 1), 'before1', InlineDecorationType.Before), + new InlineDecoration(new Range(1, 1, 1, 1), 'after1', InlineDecorationType.After) ]); }); }); diff --git a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts index 6fe30a128..d556f3da7 100644 --- a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts +++ b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts @@ -18,7 +18,7 @@ suite('ViewModel', () => { lineNumbersMinChars: 1 }; testViewModel(text, opts, (viewModel, model) => { - assert.equal(viewModel.getLineCount(), 1); + assert.strictEqual(viewModel.getLineCount(), 1); viewModel.setViewport(1, 1, 1); @@ -38,14 +38,14 @@ suite('ViewModel', () => { ].join('\n') }]); - assert.equal(viewModel.getLineCount(), 10); + assert.strictEqual(viewModel.getLineCount(), 10); }); }); test('issue #44805: SplitLinesCollection: attempt to access a \'newer\' model', () => { const text = ['']; testViewModel(text, {}, (viewModel, model) => { - assert.equal(viewModel.getLineCount(), 1); + assert.strictEqual(viewModel.getLineCount(), 1); model.pushEditOperations([], [{ range: new Range(1, 1, 1, 1), @@ -74,7 +74,7 @@ suite('ViewModel', () => { model.undo(); viewLineCount.push(viewModel.getLineCount()); - assert.deepEqual(viewLineCount, [4, 1, 1, 1, 1]); + assert.deepStrictEqual(viewLineCount, [4, 1, 1, 1, 1]); }); }); @@ -85,7 +85,7 @@ suite('ViewModel', () => { 'line3' ]; testViewModel(text, {}, (viewModel, model) => { - assert.equal(viewModel.getLineCount(), 3); + assert.strictEqual(viewModel.getLineCount(), 3); viewModel.setHiddenAreas([new Range(1, 1, 3, 1)]); assert.ok(viewModel.getVisibleRanges() !== null); }); @@ -96,7 +96,7 @@ suite('ViewModel', () => { '' ]; testViewModel(text, {}, (viewModel, model) => { - assert.equal(viewModel.getLineCount(), 1); + assert.strictEqual(viewModel.getLineCount(), 1); model.pushEditOperations([], [{ range: new Range(1, 1, 1, 1), @@ -104,7 +104,7 @@ suite('ViewModel', () => { }], () => ([])); viewModel.setHiddenAreas([new Range(1, 1, 1, 1)]); - assert.equal(viewModel.getLineCount(), 2); + assert.strictEqual(viewModel.getLineCount(), 2); model.undo(); assert.ok(viewModel.getVisibleRanges() !== null); @@ -114,7 +114,7 @@ suite('ViewModel', () => { function assertGetPlainTextToCopy(text: string[], ranges: Range[], emptySelectionClipboard: boolean, expected: string | string[]): void { testViewModel(text, {}, (viewModel, model) => { let actual = viewModel.getPlainTextToCopy(ranges, emptySelectionClipboard, false); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } @@ -259,7 +259,39 @@ suite('ViewModel', () => { testViewModel(USUAL_TEXT, {}, (viewModel, model) => { model.setEOL(EndOfLineSequence.LF); let actual = viewModel.getPlainTextToCopy([new Range(2, 1, 5, 1)], true, true); - assert.deepEqual(actual, 'line2\r\nline3\r\nline4\r\n'); + assert.deepStrictEqual(actual, 'line2\r\nline3\r\nline4\r\n'); }); }); + + test('issue #40926: Incorrect spacing when inserting new line after multiple folded blocks of code', () => { + testViewModel( + [ + 'foo = {', + ' foobar: function() {', + ' this.foobar();', + ' },', + ' foobar: function() {', + ' this.foobar();', + ' },', + ' foobar: function() {', + ' this.foobar();', + ' },', + '}', + ], {}, (viewModel, model) => { + viewModel.setHiddenAreas([ + new Range(3, 1, 3, 1), + new Range(6, 1, 6, 1), + new Range(9, 1, 9, 1), + ]); + + model.applyEdits([ + { range: new Range(4, 7, 4, 7), text: '\n ' }, + { range: new Range(7, 7, 7, 7), text: '\n ' }, + { range: new Range(10, 7, 10, 7), text: '\n ' } + ]); + + assert.strictEqual(viewModel.getLineCount(), 11); + } + ); + }); }); diff --git a/src/vs/editor/test/node/classification/typescript.test.ts b/src/vs/editor/test/node/classification/typescript.test.ts index 5e399061a..19c7b901e 100644 --- a/src/vs/editor/test/node/classification/typescript.test.ts +++ b/src/vs/editor/test/node/classification/typescript.test.ts @@ -126,7 +126,7 @@ function executeTest(fileName: string, parseFunc: IParseFunc): void { actual[3 * actualIndex] + actual[3 * actualIndex + 1] >= assertion.startOffset + assertion.length, `Line ${assertion.testLineNumber} : length : ${actual[3 * actualIndex]} + ${actual[3 * actualIndex + 1]} >= ${assertion.startOffset} + ${assertion.length}.` ); - assert.equal( + assert.strictEqual( actual[3 * actualIndex + 2], assertion.tokenType, `Line ${assertion.testLineNumber} : tokenType`); diff --git a/src/vs/loader.js b/src/vs/loader.js index 76db97736..a9ac0018c 100644 --- a/src/vs/loader.js +++ b/src/vs/loader.js @@ -36,7 +36,7 @@ var AMDLoader; this._detect(); return this._isWindows; }, - enumerable: true, + enumerable: false, configurable: true }); Object.defineProperty(Environment.prototype, "isNode", { @@ -44,7 +44,7 @@ var AMDLoader; this._detect(); return this._isNode; }, - enumerable: true, + enumerable: false, configurable: true }); Object.defineProperty(Environment.prototype, "isElectronRenderer", { @@ -52,7 +52,7 @@ var AMDLoader; this._detect(); return this._isElectronRenderer; }, - enumerable: true, + enumerable: false, configurable: true }); Object.defineProperty(Environment.prototype, "isWebWorker", { @@ -60,7 +60,7 @@ var AMDLoader; this._detect(); return this._isWebWorker; }, - enumerable: true, + enumerable: false, configurable: true }); Environment.prototype._detect = function () { @@ -199,6 +199,7 @@ var AMDLoader; return obj; } if (!Array.isArray(obj) && Object.getPrototypeOf(obj) !== Object.prototype) { + // only clone "simple" objects return obj; } var result = Array.isArray(obj) ? [] : {}; @@ -740,8 +741,8 @@ var AMDLoader; // nothing } }; - require.resolve = function resolve(request) { - return Module._resolveFilename(request, mod); + require.resolve = function resolve(request, options) { + return Module._resolveFilename(request, mod, false, options); }; require.main = process.mainModule; require.extensions = Module._extensions; @@ -862,7 +863,7 @@ var AMDLoader; } }; NodeScriptLoader.prototype._getCachedDataPath = function (config, filename) { - var hash = this._crypto.createHash('md5').update(filename, 'utf8').update(config.seed, 'utf8').digest('hex'); + var hash = this._crypto.createHash('md5').update(filename, 'utf8').update(config.seed, 'utf8').update(process.arch, '').digest('hex'); var basename = this._path.basename(filename).replace(/\.js$/, ''); return this._path.join(config.path, basename + "-" + hash + ".code"); }; @@ -1217,6 +1218,7 @@ var AMDLoader; this._requireFunc = requireFunc; this._moduleIdProvider = new ModuleIdProvider(); this._config = new AMDLoader.Configuration(this._env); + this._hasDependencyCycle = false; this._modules2 = []; this._knownModules2 = []; this._inverseDependencies2 = []; @@ -1561,6 +1563,9 @@ var AMDLoader; result.getStats = function () { return _this.getLoaderEvents(); }; + result.hasDependencyCycle = function () { + return _this._hasDependencyCycle; + }; result.config = function (params, shouldOverwrite) { if (shouldOverwrite === void 0) { shouldOverwrite = false; } _this.configure(params, shouldOverwrite); @@ -1666,6 +1671,7 @@ var AMDLoader; continue; } if (this._hasDependencyPath(dependency.id, module.id)) { + this._hasDependencyCycle = true; console.warn('There is a dependency cycle between \'' + this._moduleIdProvider.getStrModuleId(dependency.id) + '\' and \'' + this._moduleIdProvider.getStrModuleId(module.id) + '\'. The cyclic path follows:'); var cyclePath = this._findCyclePath(dependency.id, module.id, 0) || []; cyclePath.reverse(); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 2cb6913f7..227452ee5 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -10,6 +10,7 @@ declare namespace monaco { export type Thenable = PromiseLike; export interface Environment { + globalAPI?: boolean; baseUrl?: string; getWorker?(workerId: string, label: string): Worker; getWorkerUrl?(workerId: string, label: string): string; @@ -897,6 +898,12 @@ declare namespace monaco.editor { take?: number; }): IMarker[]; + /** + * Emitted when markers change for a model. + * @event + */ + export function onDidChangeMarkers(listener: (e: readonly Uri[]) => void): IDisposable; + /** * Get the model that has `uri` if it exists. */ @@ -969,6 +976,11 @@ declare namespace monaco.editor { */ export function remeasureFonts(): void; + /** + * Register a command. + */ + export function registerCommand(id: string, handler: (accessor: any, ...args: any[]) => void): IDisposable; + export type BuiltinTheme = 'vs' | 'vs-dark' | 'hc-black'; export interface IStandaloneThemeData { @@ -3176,6 +3188,10 @@ declare namespace monaco.editor { * Controls strikethrough deprecated variables. */ showDeprecated?: boolean; + /** + * Control the behavior and rendering of the inline hints. + */ + inlineHints?: IEditorInlineHintsOptions; } /** @@ -3222,6 +3238,11 @@ declare namespace monaco.editor { * Defaults to false */ isInEmbeddedEditor?: boolean; + /** + * Is the diff editor should render overview ruler + * Defaults to true + */ + renderOverviewRuler?: boolean; /** * Control the wrapping of the diff editor. */ @@ -3522,6 +3543,29 @@ declare namespace monaco.editor { export type EditorLightbulbOptions = Readonly>; + /** + * Configuration options for editor inlineHints + */ + export interface IEditorInlineHintsOptions { + /** + * Enable the inline hints. + * Defaults to true. + */ + enabled?: boolean; + /** + * Font size of inline hints. + * Default to 90% of the editor font size. + */ + fontSize?: number; + /** + * Font family of inline hints. + * Defaults to editor font family. + */ + fontFamily?: string; + } + + export type EditorInlineHintsOptions = Readonly>; + /** * Configuration options for editor minimap */ @@ -4023,11 +4067,12 @@ declare namespace monaco.editor { wrappingIndent = 117, wrappingStrategy = 118, showDeprecated = 119, - editorClassName = 120, - pixelRatio = 121, - tabFocusMode = 122, - layoutInfo = 123, - wrappingInfo = 124 + inlineHints = 120, + editorClassName = 121, + pixelRatio = 122, + tabFocusMode = 123, + layoutInfo = 124, + wrappingInfo = 125 } export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; @@ -4128,6 +4173,7 @@ declare namespace monaco.editor { showFoldingControls: IEditorOption; showUnused: IEditorOption; showDeprecated: IEditorOption; + inlineHints: IEditorOption; snippetSuggestions: IEditorOption; smartSelect: IEditorOption; smoothScrolling: IEditorOption; @@ -4492,6 +4538,10 @@ declare namespace monaco.editor { } export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { + /** + * The initial editor dimension (to avoid measuring the container). + */ + dimension?: IDimension; /** * Place overflow widgets inside an external DOM node. * Defaults to an internal DOM node. @@ -4935,6 +4985,7 @@ declare namespace monaco.editor { export class FontInfo extends BareFontInfo { readonly _editorStylingBrand: void; + readonly version: number; readonly isTrusted: boolean; readonly isMonospace: boolean; readonly typicalHalfwidthCharacterWidth: number; @@ -4949,6 +5000,7 @@ declare namespace monaco.editor { export class BareFontInfo { readonly _bareFontInfoBrand: void; readonly zoomLevel: number; + readonly pixelRatio: number; readonly fontFamily: string; readonly fontWeight: string; readonly fontSize: number; @@ -5075,6 +5127,12 @@ declare namespace monaco.languages { tokenize?(line: string, state: IState): ILineTokens; } + /** + * Change the color map that is used for token colors. + * Supported formats (hex): #RRGGBB, $RRGGBBAA, #RGB, #RGBA + */ + export function setColorMap(colorMap: string[] | null): void; + /** * Set the tokens provider for a language (manual implementation). */ @@ -5366,7 +5424,7 @@ declare namespace monaco.languages { /** * This rule will only execute if the text above the this line matches this regular expression. */ - oneLineAboveText?: RegExp; + previousLineText?: RegExp; /** * The action to execute. */ @@ -6352,6 +6410,19 @@ declare namespace monaco.languages { resolveCodeLens?(model: editor.ITextModel, codeLens: CodeLens, token: CancellationToken): ProviderResult; } + export interface InlineHint { + text: string; + range: IRange; + description?: string | IMarkdownString; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; + } + + export interface InlineHintsProvider { + onDidChangeInlineHints?: IEvent | undefined; + provideInlineHints(model: editor.ITextModel, range: Range, token: CancellationToken): ProviderResult; + } + export interface SemanticTokensLegend { readonly tokenTypes: string[]; readonly tokenModifiers: string[]; @@ -6429,6 +6500,11 @@ declare namespace monaco.languages { * attach this to every token class (by default '.' + name) */ tokenPostfix?: string; + /** + * include line feeds (in the form of a \n character) at the end of lines + * Defaults to false + */ + includeLF?: boolean; } /** diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.css b/src/vs/platform/actions/browser/menuEntryActionViewItem.css new file mode 100644 index 000000000..5ef85311a --- /dev/null +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.css @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-action-bar .action-item.menu-entry .action-label { + background-image: var(--menu-entry-icon-light); + display: inline-flex; +} + +.vs-dark .monaco-action-bar .action-item.menu-entry .action-label, +.hc-black .monaco-action-bar .action-item.menu-entry .action-label { + background-image: var(--menu-entry-icon-dark); +} diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 1344ea900..fbfdd035a 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createCSSRule, asCSSUrl, ModifierKeyEmitter } from 'vs/base/browser/dom'; +import 'vs/css!./menuEntryActionViewItem'; +import { asCSSUrl, ModifierKeyEmitter } from 'vs/base/browser/dom'; import { domEvent } from 'vs/base/browser/event'; import { IAction, Separator } from 'vs/base/common/actions'; -import { IdGenerator } from 'vs/base/common/idGenerator'; import { IDisposable, toDisposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { ICommandAction, IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction, Icon } from 'vs/platform/actions/common/actions'; @@ -17,6 +17,7 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { isWindows, isLinux } from 'vs/base/common/platform'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable { const groups = menu.getActions(options); @@ -44,8 +45,7 @@ function asDisposable(groups: ReadonlyArray<[string, ReadonlyArray]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void { - for (let tuple of groups) { - let [group, actions] = tuple; + for (let [group, actions] of groups) { if (useAlternativeActions) { actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a); } @@ -66,10 +66,6 @@ function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray(); - export class MenuEntryActionViewItem extends ActionViewItem { private _wantsAltCommand: boolean = false; @@ -85,7 +81,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { this._altKey = ModifierKeyEmitter.getInstance(); } - protected get _commandAction(): IAction { + protected get _commandAction(): MenuItemAction { return this._wantsAltCommand && (this._action).alt || this._action; } @@ -93,12 +89,14 @@ export class MenuEntryActionViewItem extends ActionViewItem { event.preventDefault(); event.stopPropagation(); - this.actionRunner.run(this._commandAction, this._context) - .then(undefined, err => this._notificationService.error(err)); + this.actionRunner + .run(this._commandAction, this._context) + .catch(err => this._notificationService.error(err)); } render(container: HTMLElement): void { super.render(container); + container.classList.add('menu-entry'); this._updateItemClass(this._action.item); @@ -167,46 +165,39 @@ export class MenuEntryActionViewItem extends ActionViewItem { private _updateItemClass(item: ICommandAction): void { this._itemClassDispose.value = undefined; + const { element, label } = this; + if (!element || !label) { + return; + } + const icon = this._commandAction.checked && (item.toggled as { icon?: Icon })?.icon ? (item.toggled as { icon: Icon }).icon : item.icon; + if (!icon) { + return; + } + if (ThemeIcon.isThemeIcon(icon)) { // theme icons const iconClass = ThemeIcon.asClassName(icon); - if (this.label && iconClass) { - this.label.classList.add(...iconClass.split(' ')); - this._itemClassDispose.value = toDisposable(() => { - if (this.label) { - this.label.classList.remove(...iconClass.split(' ')); - } - }); + label.classList.add(...iconClass.split(' ')); + this._itemClassDispose.value = toDisposable(() => { + label.classList.remove(...iconClass.split(' ')); + }); + + } else { + // icon path/url + if (icon.light) { + label.style.setProperty('--menu-entry-icon-light', asCSSUrl(icon.light)); } - - } else if (icon) { - // icon path - let iconClass: string; - - if (icon.dark?.scheme) { - - const iconPathMapKey = icon.dark.toString(); - - if (ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) { - iconClass = ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!; - } else { - iconClass = ids.nextId(); - createCSSRule(`.icon.${iconClass}`, `background-image: ${asCSSUrl(icon.light || icon.dark)}`); - createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: ${asCSSUrl(icon.dark)}`); - ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, iconClass); - } - - if (this.label) { - this.label.classList.add('icon', ...iconClass.split(' ')); - this._itemClassDispose.value = toDisposable(() => { - if (this.label) { - this.label.classList.remove('icon', ...iconClass.split(' ')); - } - }); - } + if (icon.dark) { + label.style.setProperty('--menu-entry-icon-dark', asCSSUrl(icon.dark)); } + label.classList.add('icon'); + this._itemClassDispose.value = toDisposable(() => { + label.classList.remove('icon'); + label.style.removeProperty('--menu-entry-icon-light'); + label.style.removeProperty('--menu-entry-icon-dark'); + }); } } } @@ -215,29 +206,41 @@ export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem { constructor( action: SubmenuItemAction, - @INotificationService _notificationService: INotificationService, - @IContextMenuService _contextMenuService: IContextMenuService + @IContextMenuService contextMenuService: IContextMenuService ) { - let classNames: string | string[] | undefined; + super(action, { getActions: () => action.actions }, contextMenuService, { + menuAsChild: true, + classNames: ThemeIcon.isThemeIcon(action.item.icon) ? ThemeIcon.asClassName(action.item.icon) : undefined, + }); + } - if (action.item.icon) { - if (ThemeIcon.isThemeIcon(action.item.icon)) { - classNames = ThemeIcon.asClassName(action.item.icon)!; - } else if (action.item.icon.dark?.scheme) { - const iconPathMapKey = action.item.icon.dark.toString(); - - if (ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) { - classNames = ['icon', ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!]; - } else { - const className = ids.nextId(); - classNames = ['icon', className]; - createCSSRule(`.icon.${className}`, `background-image: ${asCSSUrl(action.item.icon.light || action.item.icon.dark)}`); - createCSSRule(`.vs-dark .icon.${className}, .hc-black .icon.${className}`, `background-image: ${asCSSUrl(action.item.icon.dark)}`); - ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, className); + render(container: HTMLElement): void { + super.render(container); + if (this.element) { + container.classList.add('menu-entry'); + const { icon } = (this._action).item; + if (icon && !ThemeIcon.isThemeIcon(icon)) { + this.element.classList.add('icon'); + if (icon.light) { + this.element.style.setProperty('--menu-entry-icon-light', asCSSUrl(icon.light)); + } + if (icon.dark) { + this.element.style.setProperty('--menu-entry-icon-dark', asCSSUrl(icon.dark)); } } } - - super(action, action.actions, _contextMenuService, { classNames: classNames, menuAsChild: true }); + } +} + +/** + * Creates action view items for menu actions or submenu actions. + */ +export function createActionViewItem(instaService: IInstantiationService, action: IAction): undefined | MenuEntryActionViewItem | SubmenuEntryActionViewItem { + if (action instanceof MenuItemAction) { + return instaService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return instaService.createInstance(SubmenuEntryActionViewItem, action); + } else { + return undefined; } } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index c06a3511b..514fc0bd6 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -16,22 +16,36 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { UriDto } from 'vs/base/common/types'; import { Iterable } from 'vs/base/common/iterator'; import { LinkedList } from 'vs/base/common/linkedList'; +import { CSSIcon } from 'vs/base/common/codicons'; export interface ILocalizedString { + /** + * The localized value of the string. + */ value: string; + /** + * The original (non localized value of the string) + */ original: string; } +export interface ICommandActionTitle extends ILocalizedString { + /** + * The title with a mnemonic designation. && precedes the mnemonic. + */ + mnemonicTitle?: string; +} + export type Icon = { dark?: URI; light?: URI; } | ThemeIcon; export interface ICommandAction { id: string; - title: string | ILocalizedString; + title: string | ICommandActionTitle; category?: string | ILocalizedString; - tooltip?: string | ILocalizedString; + tooltip?: string; icon?: Icon; precondition?: ContextKeyExpression; - toggled?: ContextKeyExpression | { condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString }; + toggled?: ContextKeyExpression | { condition: ContextKeyExpression, icon?: Icon, tooltip?: string }; } export type ISerializableCommandAction = UriDto; @@ -45,7 +59,7 @@ export interface IMenuItem { } export interface ISubmenuItem { - title: string | ILocalizedString; + title: string | ICommandActionTitle; submenu: MenuId; icon?: Icon; when?: ContextKeyExpression; @@ -112,6 +126,7 @@ export class MenuId { static readonly TunnelInline = new MenuId('TunnelInline'); static readonly TunnelTitle = new MenuId('TunnelTitle'); static readonly ViewItemContext = new MenuId('ViewItemContext'); + static readonly ViewContainerTitle = new MenuId('ViewContainerTitle'); static readonly ViewContainerTitleContext = new MenuId('ViewContainerTitleContext'); static readonly ViewTitle = new MenuId('ViewTitle'); static readonly ViewTitleContext = new MenuId('ViewTitleContext'); @@ -132,6 +147,7 @@ export class MenuId { static readonly TimelineTitle = new MenuId('TimelineTitle'); static readonly TimelineTitleContext = new MenuId('TimelineTitleContext'); static readonly AccountsContext = new MenuId('AccountsContext'); + static readonly PanelTitle = new MenuId('PanelTitle'); readonly id: number; readonly _debugName: string; @@ -148,7 +164,7 @@ export interface IMenuActionOptions { } export interface IMenu extends IDisposable { - readonly onDidChange: Event; + readonly onDidChange: Event; getActions(options?: IMenuActionOptions): [string, Array][]; } @@ -334,61 +350,71 @@ export class SubmenuItemAction extends SubmenuAction { } } -export class MenuItemAction extends ExecuteCommandAction { +// implements IAction, does NOT extend Action, so that no one +// subscribes to events of Action or modified properties +export class MenuItemAction implements IAction { readonly item: ICommandAction; readonly alt: MenuItemAction | undefined; - private _options: IMenuActionOptions; + private readonly _options: IMenuActionOptions | undefined; + + readonly id: string; + readonly label: string; + readonly tooltip: string; + readonly class: string | undefined; + readonly enabled: boolean; + readonly checked: boolean; constructor( item: ICommandAction, alt: ICommandAction | undefined, - options: IMenuActionOptions, + options: IMenuActionOptions | undefined, @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService commandService: ICommandService + @ICommandService private _commandService: ICommandService ) { - typeof item.title === 'string' ? super(item.id, item.title, commandService) : super(item.id, item.title.value, commandService); - - this._cssClass = undefined; - this._enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition); - this._tooltip = item.tooltip ? typeof item.tooltip === 'string' ? item.tooltip : item.tooltip.value : undefined; + this.id = item.id; + this.label = typeof item.title === 'string' ? item.title : item.title.value; + this.tooltip = item.tooltip ?? ''; + this.enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition); + this.checked = false; if (item.toggled) { const toggled = ((item.toggled as { condition: ContextKeyExpression }).condition ? item.toggled : { condition: item.toggled }) as { condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString }; - this._checked = contextKeyService.contextMatchesRules(toggled.condition); - if (this._checked && toggled.tooltip) { - this._tooltip = typeof toggled.tooltip === 'string' ? toggled.tooltip : toggled.tooltip.value; + this.checked = contextKeyService.contextMatchesRules(toggled.condition); + if (this.checked && toggled.tooltip) { + this.tooltip = typeof toggled.tooltip === 'string' ? toggled.tooltip : toggled.tooltip.value; } } - this._options = options || {}; - this.item = item; - this.alt = alt ? new MenuItemAction(alt, undefined, this._options, contextKeyService, commandService) : undefined; + this.alt = alt ? new MenuItemAction(alt, undefined, options, contextKeyService, _commandService) : undefined; + this._options = options; + if (ThemeIcon.isThemeIcon(item.icon)) { + this.class = CSSIcon.asClassName(item.icon); + } } dispose(): void { - if (this.alt) { - this.alt.dispose(); - } - super.dispose(); + // there is NOTHING to dispose and the MenuItemAction should + // never have anything to dispose as it is a convenience type + // to bridge into the rendering world. } run(...args: any[]): Promise { let runArgs: any[] = []; - if (this._options.arg) { + if (this._options?.arg) { runArgs = [...runArgs, this._options.arg]; } - if (this._options.shouldForwardArgs) { + if (this._options?.shouldForwardArgs) { runArgs = [...runArgs, ...args]; } - return super.run(...runArgs); + return this._commandService.executeCommand(this.id, ...runArgs); } } diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index 6ea4c5a08..eaabf117b 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { RunOnceScheduler } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IMenu, IMenuActionOptions, IMenuItem, IMenuService, isIMenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction, ILocalizedString } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IContextKeyService, IContextKeyChangeEvent, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; export class MenuService implements IMenuService { @@ -29,9 +30,11 @@ type MenuItemGroup = [string, Array]; class Menu implements IMenu { - private readonly _onDidChange = new Emitter(); private readonly _dispoables = new DisposableStore(); + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + private _menuGroups: MenuItemGroup[] = []; private _contextKeys: Set = new Set(); @@ -45,19 +48,23 @@ class Menu implements IMenu { // rebuild this menu whenever the menu registry reports an // event for this MenuId - this._dispoables.add(Event.debounce( - Event.filter(MenuRegistry.onDidChangeMenu, set => set.has(this._id)), - () => { }, - 50 - )(this._build, this)); + const rebuildMenuSoon = new RunOnceScheduler(() => this._build(), 50); + this._dispoables.add(rebuildMenuSoon); + this._dispoables.add(MenuRegistry.onDidChangeMenu(e => { + if (e.has(_id)) { + rebuildMenuSoon.schedule(); + } + })); // when context keys change we need to check if the menu also // has changed - this._dispoables.add(Event.debounce( - this._contextKeyService.onDidChangeContext, - (last, event) => last || event.affectsSome(this._contextKeys), - 50 - )(e => e && this._onDidChange.fire(undefined), this)); + const fireChangeSoon = new RunOnceScheduler(() => this._onDidChange.fire(this), 50); + this._dispoables.add(fireChangeSoon); + this._dispoables.add(_contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(this._contextKeys)) { + fireChangeSoon.schedule(); + } + })); } dispose(): void { @@ -88,25 +95,22 @@ class Menu implements IMenu { // keep keys for eventing Menu._fillInKbExprKeys(item.when, this._contextKeys); - // keep precondition keys for event if applicable - if (isIMenuItem(item) && item.command.precondition) { - Menu._fillInKbExprKeys(item.command.precondition, this._contextKeys); - } - - // keep toggled keys for event if applicable - if (isIMenuItem(item) && item.command.toggled) { - const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled; - Menu._fillInKbExprKeys(toggledExpression, this._contextKeys); + if (isIMenuItem(item)) { + // keep precondition keys for event if applicable + if (item.command.precondition) { + Menu._fillInKbExprKeys(item.command.precondition, this._contextKeys); + } + // keep toggled keys for event if applicable + if (item.command.toggled) { + const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled; + Menu._fillInKbExprKeys(toggledExpression, this._contextKeys); + } } } this._onDidChange.fire(this); } - get onDidChange(): Event { - return this._onDidChange.event; - } - - getActions(options: IMenuActionOptions): [string, Array][] { + getActions(options?: IMenuActionOptions): [string, Array][] { const result: [string, Array][] = []; for (let group of this._menuGroups) { const [id, items] = group; diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 42e2636d8..75b2cac37 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -20,39 +20,14 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { ConsoleLogMainService } from 'vs/platform/log/common/log'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { createHash } from 'crypto'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; -suite('BackupMainService', () => { +flakySuite('BackupMainService', () => { function assertEqualUris(actual: URI[], expected: URI[]) { - assert.deepEqual(actual.map(a => a.toString()), expected.map(a => a.toString())); - } - - const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupservice'); - const backupHome = path.join(parentDir, 'Backups'); - const backupWorkspacesPath = path.join(backupHome, 'workspaces.json'); - - const environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS)); - - class TestBackupMainService extends BackupMainService { - - constructor(backupHome: string, backupWorkspacesPath: string, configService: TestConfigurationService) { - super(environmentService, configService, new ConsoleLogMainService()); - - this.backupHome = backupHome; - this.workspacesJsonPath = backupWorkspacesPath; - } - - toBackupPath(arg: URI | string): string { - const id = arg instanceof URI ? super.getFolderHash(arg) : arg; - return path.join(this.backupHome, id); - } - - getFolderHash(folderUri: URI): string { - return super.getFolderHash(folderUri); - } + assert.deepStrictEqual(actual.map(a => a.toString()), expected.map(a => a.toString())); } function toWorkspace(path: string): IWorkspaceIdentifier { @@ -79,20 +54,23 @@ suite('BackupMainService', () => { }; } - async function ensureFolderExists(uri: URI): Promise { + function ensureFolderExists(uri: URI): Promise { if (!fs.existsSync(uri.fsPath)) { fs.mkdirSync(uri.fsPath); } + const backupFolder = service.toBackupPath(uri); - await createBackupFolder(backupFolder); + return createBackupFolder(backupFolder); } async function ensureWorkspaceExists(workspace: IWorkspaceIdentifier): Promise { if (!fs.existsSync(workspace.configPath.fsPath)) { await pfs.writeFile(workspace.configPath.fsPath, 'Hello'); } + const backupFolder = service.toBackupPath(workspace.id); await createBackupFolder(backupFolder); + return workspace; } @@ -111,29 +89,52 @@ suite('BackupMainService', () => { const fooFile = URI.file(platform.isWindows ? 'C:\\foo' : '/foo'); const barFile = URI.file(platform.isWindows ? 'C:\\bar' : '/bar'); - const existingTestFolder1 = URI.file(path.join(parentDir, 'folder1')); - - let service: TestBackupMainService; + let service: BackupMainService & { toBackupPath(arg: URI | string): string, getFolderHash(folderUri: URI): string }; let configService: TestConfigurationService; - setup(async () => { + let environmentService: EnvironmentMainService; + let testDir: string; + let backupHome: string; + let backupWorkspacesPath: string; + let existingTestFolder1: URI; + + setup(async () => { + testDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupmainservice'); + backupHome = path.join(testDir, 'Backups'); + backupWorkspacesPath = path.join(backupHome, 'workspaces.json'); + existingTestFolder1 = URI.file(path.join(testDir, 'folder1')); + + environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS)); - // Delete any existing backups completely and then re-create it. - await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); await pfs.mkdirp(backupHome); configService = new TestConfigurationService(); - service = new TestBackupMainService(backupHome, backupWorkspacesPath, configService); + service = new class TestBackupMainService extends BackupMainService { + constructor() { + super(environmentService, configService, new ConsoleLogMainService()); + + this.backupHome = backupHome; + this.workspacesJsonPath = backupWorkspacesPath; + } + + toBackupPath(arg: URI | string): string { + const id = arg instanceof URI ? super.getFolderHash(arg) : arg; + return path.join(this.backupHome, id); + } + + getFolderHash(folderUri: URI): string { + return super.getFolderHash(folderUri); + } + }; return service.initialize(); }); teardown(() => { - return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); + return pfs.rimraf(testDir); }); test('service validates backup workspaces on startup and cleans up (folder workspaces)', async function () { - this.timeout(1000 * 10); // increase timeout for this test // 1) backup workspace path does not exist service.registerFolderBackupSync(fooFile); @@ -170,22 +171,21 @@ suite('BackupMainService', () => { fs.mkdirSync(service.toBackupPath(barFile)); fs.mkdirSync(fileBackups); service.registerFolderBackupSync(fooFile); - assert.equal(service.getFolderBackupPaths().length, 1); - assert.equal(service.getEmptyWindowBackupPaths().length, 0); + assert.strictEqual(service.getFolderBackupPaths().length, 1); + assert.strictEqual(service.getEmptyWindowBackupPaths().length, 0); fs.writeFileSync(path.join(fileBackups, 'backup.txt'), ''); await service.initialize(); - assert.equal(service.getFolderBackupPaths().length, 0); - assert.equal(service.getEmptyWindowBackupPaths().length, 1); + assert.strictEqual(service.getFolderBackupPaths().length, 0); + assert.strictEqual(service.getEmptyWindowBackupPaths().length, 1); }); test('service validates backup workspaces on startup and cleans up (root workspaces)', async function () { - this.timeout(1000 * 10); // increase timeout for this test // 1) backup workspace path does not exist service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath)); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath)); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); // 2) backup workspace path exists with empty contents within fs.mkdirSync(service.toBackupPath(fooFile)); @@ -193,7 +193,7 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath)); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath)); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); assert.ok(!fs.existsSync(service.toBackupPath(barFile))); @@ -205,7 +205,7 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath)); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath)); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); assert.ok(!fs.existsSync(service.toBackupPath(barFile))); @@ -216,12 +216,12 @@ suite('BackupMainService', () => { fs.mkdirSync(service.toBackupPath(barFile)); fs.mkdirSync(fileBackups); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath)); - assert.equal(service.getWorkspaceBackups().length, 1); - assert.equal(service.getEmptyWindowBackupPaths().length, 0); + assert.strictEqual(service.getWorkspaceBackups().length, 1); + assert.strictEqual(service.getEmptyWindowBackupPaths().length, 0); fs.writeFileSync(path.join(fileBackups, 'backup.txt'), ''); await service.initialize(); - assert.equal(service.getWorkspaceBackups().length, 0); - assert.equal(service.getEmptyWindowBackupPaths().length, 1); + assert.strictEqual(service.getWorkspaceBackups().length, 0); + assert.strictEqual(service.getEmptyWindowBackupPaths().length, 1); }); test('service supports to migrate backup data from another location', () => { @@ -237,7 +237,7 @@ suite('BackupMainService', () => { assert.ok(!fs.existsSync(backupPathToMigrate)); const emptyBackups = service.getEmptyWindowBackupPaths(); - assert.equal(0, emptyBackups.length); + assert.strictEqual(0, emptyBackups.length); }); test('service backup migration makes sure to preserve existing backups', () => { @@ -258,8 +258,8 @@ suite('BackupMainService', () => { assert.ok(!fs.existsSync(backupPathToMigrate)); const emptyBackups = service.getEmptyWindowBackupPaths(); - assert.equal(1, emptyBackups.length); - assert.equal(1, fs.readdirSync(path.join(backupHome, emptyBackups[0].backupFolder!)).length); + assert.strictEqual(1, emptyBackups.length); + assert.strictEqual(1, fs.readdirSync(path.join(backupHome, emptyBackups[0].backupFolder!)).length); }); suite('loadSync', () => { @@ -315,121 +315,120 @@ suite('BackupMainService', () => { }); test('getWorkspaceBackups() should return [] when workspaces.json doesn\'t exist', () => { - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when workspaces.json is not properly formed JSON', async () => { fs.writeFileSync(backupWorkspacesPath, ''); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{]'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, 'foo'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when folderWorkspaces in workspaces.json is absent', async () => { fs.writeFileSync(backupWorkspacesPath, '{}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when rootWorkspaces in workspaces.json is not a object array', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": ["bar"]}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": []}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": "bar"}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":"foo"}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":1}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when rootURIWorkspaces in workspaces.json is not a object array', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": ["bar"]}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": []}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": "bar"}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":"foo"}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":1}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when files.hotExit = "onExitAndWindowClose"', async () => { const upperFooPath = fooFile.fsPath.toUpperCase(); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath)); - assert.equal(service.getWorkspaceBackups().length, 1); + assert.strictEqual(service.getWorkspaceBackups().length, 1); assertEqualUris(service.getWorkspaceBackups().map(r => r.workspace.configPath), [URI.file(upperFooPath)]); configService.setUserConfiguration('files.hotExit', HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json doesn\'t exist', () => { - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json is not properly formed JSON', async () => { fs.writeFileSync(backupWorkspacesPath, ''); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{]'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, 'foo'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is absent', async () => { fs.writeFileSync(backupWorkspacesPath, '{}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array', async function () { - this.timeout(5000); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{}}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": ["bar"]}}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": []}}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": "bar"}}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":"foo"}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":1}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); }); }); @@ -448,7 +447,7 @@ suite('BackupMainService', () => { const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); test('should ignore duplicates on Windows and Mac (folder workspace)', async () => { @@ -464,14 +463,13 @@ suite('BackupMainService', () => { await service.initialize(); const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); test('should ignore duplicates on Windows and Mac (root workspace)', async () => { - - const workspacePath = path.join(parentDir, 'Foo.code-workspace'); - const workspacePath1 = path.join(parentDir, 'FOO.code-workspace'); - const workspacePath2 = path.join(parentDir, 'foo.code-workspace'); + const workspacePath = path.join(testDir, 'Foo.code-workspace'); + const workspacePath1 = path.join(testDir, 'FOO.code-workspace'); + const workspacePath2 = path.join(testDir, 'foo.code-workspace'); const workspace1 = await ensureWorkspaceExists(toWorkspace(workspacePath)); const workspace2 = await ensureWorkspaceExists(toWorkspace(workspacePath1)); @@ -487,11 +485,11 @@ suite('BackupMainService', () => { const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.equal(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1); + assert.strictEqual(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1); if (platform.isLinux) { - assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString(), URI.file(workspacePath1).toString(), URI.file(workspacePath2).toString()]); + assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString(), URI.file(workspacePath1).toString(), URI.file(workspacePath2).toString()]); } else { - assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString()], 'should return the first duplicated entry'); + assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString()], 'should return the first duplicated entry'); } }); }); @@ -503,7 +501,7 @@ suite('BackupMainService', () => { assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]); const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]); }); test('should persist paths to workspaces.json (root workspace)', async () => { @@ -513,15 +511,15 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(ws2); assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [fooFile, barFile]); - assert.equal(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id); - assert.equal(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id); + assert.strictEqual(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id); + assert.strictEqual(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id); const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]); - assert.equal(ws1.workspace.id, json.rootURIWorkspaces[0].id); - assert.equal(ws2.workspace.id, json.rootURIWorkspaces[1].id); + assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]); + assert.strictEqual(ws1.workspace.id, json.rootURIWorkspaces[0].id); + assert.strictEqual(ws2.workspace.id, json.rootURIWorkspaces[1].id); }); }); @@ -531,7 +529,7 @@ suite('BackupMainService', () => { const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]); }); test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (root workspace)', async () => { @@ -541,7 +539,7 @@ suite('BackupMainService', () => { const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); - assert.deepEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]); + assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]); }); suite('removeBackupPathSync', () => { @@ -552,12 +550,12 @@ suite('BackupMainService', () => { const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); - assert.deepEqual(json.folderURIWorkspaces, [barFile.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [barFile.toString()]); service.unregisterFolderBackupSync(barFile); const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); - assert.deepEqual(json2.folderURIWorkspaces, []); + assert.deepStrictEqual(json2.folderURIWorkspaces, []); }); test('should remove folder workspaces from workspaces.json (root workspace)', async () => { @@ -569,12 +567,12 @@ suite('BackupMainService', () => { const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); - assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]); + assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]); service.unregisterWorkspaceBackupSync(ws2.workspace); const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); - assert.deepEqual(json2.rootURIWorkspaces, []); + assert.deepStrictEqual(json2.rootURIWorkspaces, []); }); test('should remove empty workspaces from workspaces.json', async () => { @@ -584,12 +582,12 @@ suite('BackupMainService', () => { const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); - assert.deepEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]); + assert.deepStrictEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]); service.unregisterEmptyWindowBackupSync('bar'); const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); - assert.deepEqual(json2.emptyWorkspaceInfos, []); + assert.deepStrictEqual(json2.emptyWorkspaceInfos, []); }); test('should fail gracefully when removing a path that doesn\'t exist', async () => { @@ -603,24 +601,18 @@ suite('BackupMainService', () => { service.unregisterEmptyWindowBackupSync('test'); const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(content)); - assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); }); suite('getWorkspaceHash', () => { - - test('should ignore case on Windows and Mac', () => { - // Skip test on Linux - if (platform.isLinux) { - return; - } - + (platform.isLinux ? test.skip : test)('should ignore case on Windows and Mac', () => { if (platform.isMacintosh) { - assert.equal(service.getFolderHash(URI.file('/foo')), service.getFolderHash(URI.file('/FOO'))); + assert.strictEqual(service.getFolderHash(URI.file('/foo')), service.getFolderHash(URI.file('/FOO'))); } if (platform.isWindows) { - assert.equal(service.getFolderHash(URI.file('c:\\foo')), service.getFolderHash(URI.file('C:\\FOO'))); + assert.strictEqual(service.getFolderHash(URI.file('c:\\foo')), service.getFolderHash(URI.file('C:\\FOO'))); } }); }); @@ -631,9 +623,9 @@ suite('BackupMainService', () => { service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase())); if (platform.isLinux) { - assert.equal(service.getFolderBackupPaths().length, 2); + assert.strictEqual(service.getFolderBackupPaths().length, 2); } else { - assert.equal(service.getFolderBackupPaths().length, 1); + assert.strictEqual(service.getFolderBackupPaths().length, 1); } }); @@ -642,9 +634,9 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath.toUpperCase())); if (platform.isLinux) { - assert.equal(service.getWorkspaceBackups().length, 2); + assert.strictEqual(service.getWorkspaceBackups().length, 2); } else { - assert.equal(service.getWorkspaceBackups().length, 1); + assert.strictEqual(service.getWorkspaceBackups().length, 1); } }); @@ -653,16 +645,16 @@ suite('BackupMainService', () => { // same case service.registerFolderBackupSync(fooFile); service.unregisterFolderBackupSync(fooFile); - assert.equal(service.getFolderBackupPaths().length, 0); + assert.strictEqual(service.getFolderBackupPaths().length, 0); // mixed case service.registerFolderBackupSync(fooFile); service.unregisterFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase())); if (platform.isLinux) { - assert.equal(service.getFolderBackupPaths().length, 1); + assert.strictEqual(service.getFolderBackupPaths().length, 1); } else { - assert.equal(service.getFolderBackupPaths().length, 0); + assert.strictEqual(service.getFolderBackupPaths().length, 0); } }); }); @@ -674,7 +666,7 @@ suite('BackupMainService', () => { const backupWorkspaceInfo = toWorkspaceBackupInfo(fooFile.fsPath); const workspaceBackupPath = service.registerWorkspaceBackupSync(backupWorkspaceInfo); - assert.equal(((await service.getDirtyWorkspaces()).length), 0); + assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0); try { await pfs.mkdirp(path.join(folderBackupPath, Schemas.file)); @@ -683,13 +675,13 @@ suite('BackupMainService', () => { // ignore - folder might exist already } - assert.equal(((await service.getDirtyWorkspaces()).length), 0); + assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0); fs.writeFileSync(path.join(folderBackupPath, Schemas.file, '594a4a9d82a277a899d4713a5b08f504'), ''); fs.writeFileSync(path.join(workspaceBackupPath, Schemas.untitled, '594a4a9d82a277a899d4713a5b08f504'), ''); const dirtyWorkspaces = await service.getDirtyWorkspaces(); - assert.equal(dirtyWorkspaces.length, 2); + assert.strictEqual(dirtyWorkspaces.length, 2); let found = 0; for (const dirtyWorkpspace of dirtyWorkspaces) { @@ -704,7 +696,7 @@ suite('BackupMainService', () => { } } - assert.equal(found, 2); + assert.strictEqual(found, 2); }); }); }); diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index 5af10fc31..c6928a8ac 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -16,7 +16,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { IFileService } from 'vs/platform/files/common/files'; -import { dirname } from 'vs/base/common/resources'; +import { IExtUri } from 'vs/base/common/resources'; export class ConfigurationModel implements IConfigurationModel { @@ -348,11 +348,12 @@ export class UserSettings extends Disposable { constructor( private readonly userSettingsResource: URI, private readonly scopes: ConfigurationScope[] | undefined, + extUri: IExtUri, private readonly fileService: IFileService ) { super(); this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes); - this._register(this.fileService.watch(dirname(this.userSettingsResource))); + this._register(this.fileService.watch(extUri.dirname(this.userSettingsResource))); this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire())); } diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index 07680c0f2..7504cb721 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -112,6 +112,13 @@ export interface IConfigurationPropertySchema extends IJSONSchema { scope?: ConfigurationScope; included?: boolean; tags?: string[]; + /** + * When enabled this setting is ignored during sync and user can override this. + */ + ignoreSync?: boolean; + /** + * When enabled this setting is ignored during sync and user cannot override this. + */ disallowSyncIgnore?: boolean; enumItemLabels?: string[]; } diff --git a/src/vs/platform/configuration/common/configurationService.ts b/src/vs/platform/configuration/common/configurationService.ts index 2e8b85b1f..3fd1df3f5 100644 --- a/src/vs/platform/configuration/common/configurationService.ts +++ b/src/vs/platform/configuration/common/configurationService.ts @@ -12,6 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; export class ConfigurationService extends Disposable implements IConfigurationService, IDisposable { @@ -29,7 +30,7 @@ export class ConfigurationService extends Disposable implements IConfigurationSe fileService: IFileService ) { super(); - this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, fileService)); + this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, extUriBiasedIgnorePathCase, fileService)); this.configuration = new Configuration(new DefaultConfigurationModel(), new ConfigurationModel()); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reloadConfiguration(), 50)); diff --git a/src/vs/platform/contextkey/browser/contextKeyService.ts b/src/vs/platform/contextkey/browser/contextKeyService.ts index 21320c0d6..df2e5b70d 100644 --- a/src/vs/platform/contextkey/browser/contextKeyService.ts +++ b/src/vs/platform/contextkey/browser/contextKeyService.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event'; +import { Emitter, PauseableEmitter } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; -import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; import { distinct } from 'vs/base/common/objects'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -244,12 +244,14 @@ class CompositeContextKeyChangeEvent implements IContextKeyChangeEvent { } export abstract class AbstractContextKeyService implements IContextKeyService { - public _serviceBrand: undefined; + declare _serviceBrand: undefined; protected _isDisposed: boolean; - protected _onDidChangeContext = new PauseableEmitter({ merge: input => new CompositeContextKeyChangeEvent(input) }); protected _myContextId: number; + protected _onDidChangeContext = new PauseableEmitter({ merge: input => new CompositeContextKeyChangeEvent(input) }); + readonly onDidChangeContext = this._onDidChangeContext.event; + constructor(myContextId: number) { this._isDisposed = false; this._myContextId = myContextId; @@ -268,9 +270,6 @@ export abstract class AbstractContextKeyService implements IContextKeyService { return new ContextKey(this, key, defaultValue); } - public get onDidChangeContext(): Event { - return this._onDidChangeContext.event; - } bufferChangeEvents(callback: Function): void { this._onDidChangeContext.pause(); @@ -371,6 +370,7 @@ export class ContextKeyService extends AbstractContextKeyService implements ICon } public dispose(): void { + this._onDidChangeContext.dispose(); this._isDisposed = true; this._toDispose.dispose(); } @@ -407,34 +407,34 @@ class ScopedContextKeyService extends AbstractContextKeyService { private _parent: AbstractContextKeyService; private _domNode: IContextKeyServiceTarget | undefined; - private _parentChangeListener: IDisposable | undefined; + private readonly _parentChangeListener = new MutableDisposable(); constructor(parent: AbstractContextKeyService, domNode?: IContextKeyServiceTarget) { super(parent.createChildContext()); this._parent = parent; - this.updateParentChangeListener(); + this._updateParentChangeListener(); if (domNode) { this._domNode = domNode; if (this._domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) { - console.error('Element already has context attribute'); + let extraInfo = ''; + if ((this._domNode as HTMLElement).classList) { + extraInfo = Array.from((this._domNode as HTMLElement).classList.values()).join(', '); + } + + console.error(`Element already has context attribute${extraInfo ? ': ' + extraInfo : ''}`); } this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId)); } } - private updateParentChangeListener(): void { - if (this._parentChangeListener) { - this._parentChangeListener.dispose(); - } - - this._parentChangeListener = this._parent.onDidChangeContext(e => { - // Forward parent events to this listener. Parent will change. - this._onDidChangeContext.fire(e); - }); + private _updateParentChangeListener(): void { + // Forward parent events to this listener. Parent will change. + this._parentChangeListener.value = this._parent.onDidChangeContext(this._onDidChangeContext.fire, this._onDidChangeContext); } public dispose(): void { + this._onDidChangeContext.dispose(); this._isDisposed = true; this._parent.disposeContext(this._myContextId); this._parentChangeListener?.dispose(); @@ -444,10 +444,6 @@ class ScopedContextKeyService extends AbstractContextKeyService { } } - public get onDidChangeContext(): Event { - return this._onDidChangeContext.event; - } - public getContextValuesContainer(contextId: number): Context { if (this._isDisposed) { return NullContext.INSTANCE; @@ -473,7 +469,7 @@ class ScopedContextKeyService extends AbstractContextKeyService { const thisContainer = this._parent.getContextValuesContainer(this._myContextId); const oldAllValues = thisContainer.collectAllValues(); this._parent = parentContextKeyService; - this.updateParentChangeListener(); + this._updateParentChangeListener(); const newParentContainer = this._parent.getContextValuesContainer(this._parent.contextId); thisContainer.updateParent(newParentContainer); diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index ea7c83876..fde5606ea 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -1278,11 +1278,11 @@ export class RawContextKey extends ContextKeyDefinedExpr { return ContextKeyExpr.not(this.key); } - public isEqualTo(value: string): ContextKeyExpression { + public isEqualTo(value: any): ContextKeyExpression { return ContextKeyExpr.equals(this.key, value); } - public notEqualsTo(value: string): ContextKeyExpression { + public notEqualsTo(value: any): ContextKeyExpression { return ContextKeyExpr.notEquals(this.key, value); } } diff --git a/src/vs/platform/contextkey/test/browser/contextkey.test.ts b/src/vs/platform/contextkey/test/browser/contextkey.test.ts index d436b6cdf..aca26b1dd 100644 --- a/src/vs/platform/contextkey/test/browser/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/browser/contextkey.test.ts @@ -34,10 +34,10 @@ suite('ContextKeyService', () => { assert.ok(e.affectsSome(new Set(['testC'])), 'testC changed'); assert.ok(!e.affectsSome(new Set(['testD'])), 'testD did not change'); - assert.equal(child.getContextKeyValue('testA'), 3); - assert.equal(child.getContextKeyValue('testB'), undefined); - assert.equal(child.getContextKeyValue('testC'), 4); - assert.equal(child.getContextKeyValue('testD'), 0); + assert.strictEqual(child.getContextKeyValue('testA'), 3); + assert.strictEqual(child.getContextKeyValue('testB'), undefined); + assert.strictEqual(child.getContextKeyValue('testC'), 4); + assert.strictEqual(child.getContextKeyValue('testD'), 0); } catch (err) { reject(err); return; diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index 91a548be6..1abe1a076 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -67,7 +67,7 @@ suite('ContextKeyExpr', () => { function testExpression(expr: string, expected: boolean): void { // console.log(expr + ' ' + expected); let rules = ContextKeyExpr.deserialize(expr); - assert.equal(rules!.evaluate(context), expected, expr); + assert.strictEqual(rules!.evaluate(context), expected, expr); } function testBatch(expr: string, value: any): void { /* eslint-disable eqeqeq */ @@ -153,17 +153,17 @@ suite('ContextKeyExpr', () => { test('ContextKeyInExpr', () => { const ainb = ContextKeyExpr.deserialize('a in b')!; - assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [3, 2, 1] })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2, 3] })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2] })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 3 })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': null })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['x'] })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['y'] })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': {} })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': false } })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': true } })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 'prototype', 'b': {} })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [3, 2, 1] })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2, 3] })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2] })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3 })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': null })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': ['x'] })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': ['y'] })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': {} })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': false } })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': true } })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'prototype', 'b': {} })), false); }); test('issue #106524: distributing AND should normalize', () => { @@ -184,13 +184,13 @@ suite('ContextKeyExpr', () => { ContextKeyExpr.has('c') ) ); - assert.equal(actual!.equals(expected!), true); + assert.strictEqual(actual!.equals(expected!), true); }); test('Greater, GreaterEquals, Smaller, SmallerEquals evaluate', () => { function checkEvaluate(expr: string, ctx: any, expected: any): void { const _expr = ContextKeyExpr.deserialize(expr)!; - assert.equal(_expr.evaluate(createContext(ctx)), expected); + assert.strictEqual(_expr.evaluate(createContext(ctx)), expected); } checkEvaluate('a>1', {}, false); @@ -236,7 +236,7 @@ suite('ContextKeyExpr', () => { function checkNegate(expr: string, expected: string): void { const a = ContextKeyExpr.deserialize(expr)!; const b = a.negate(); - assert.equal(b.serialize(), expected); + assert.strictEqual(b.serialize(), expected); } checkNegate('a>1', 'a <= 1'); diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.ts b/src/vs/platform/contextview/browser/contextMenuHandler.ts index 1788290cc..0b515dc51 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.ts +++ b/src/vs/platform/contextview/browser/contextMenuHandler.ts @@ -158,7 +158,7 @@ export class ContextMenuHandler { } private onDidActionRun(e: IRunEvent): void { - if (e.error && this.notificationService) { + if (e.error) { this.notificationService.error(e.error); } } diff --git a/src/vs/platform/debug/common/extensionHostDebug.ts b/src/vs/platform/debug/common/extensionHostDebug.ts index b263bdd9c..b30c4e44e 100644 --- a/src/vs/platform/debug/common/extensionHostDebug.ts +++ b/src/vs/platform/debug/common/extensionHostDebug.ts @@ -5,7 +5,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; -import { IRemoteConsoleLog } from 'vs/base/common/console'; import { IProcessEnvironment } from 'vs/base/common/platform'; export const IExtensionHostDebugService = createDecorator('extensionHostDebugService'); @@ -16,11 +15,6 @@ export interface IAttachSessionEvent { port: number; } -export interface ILogToSessionEvent { - sessionId: string; - log: IRemoteConsoleLog; -} - export interface ITerminateSessionEvent { sessionId: string; subId?: string; @@ -50,9 +44,6 @@ export interface IExtensionHostDebugService { attachSession(sessionId: string, port: number, subId?: string): void; readonly onAttachSession: Event; - logToSession(sessionId: string, log: IRemoteConsoleLog): void; - readonly onLogToSession: Event; - terminateSession(sessionId: string, subId?: string): void; readonly onTerminateSession: Event; diff --git a/src/vs/platform/debug/common/extensionHostDebugIpc.ts b/src/vs/platform/debug/common/extensionHostDebugIpc.ts index 60011be13..09c2a1916 100644 --- a/src/vs/platform/debug/common/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/common/extensionHostDebugIpc.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ILogToSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; +import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; import { Event, Emitter } from 'vs/base/common/event'; -import { IRemoteConsoleLog } from 'vs/base/common/console'; import { Disposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment } from 'vs/base/common/platform'; @@ -17,7 +16,6 @@ export class ExtensionHostDebugBroadcastChannel implements IServerChan private readonly _onCloseEmitter = new Emitter(); private readonly _onReloadEmitter = new Emitter(); private readonly _onTerminateEmitter = new Emitter(); - private readonly _onLogToEmitter = new Emitter(); private readonly _onAttachEmitter = new Emitter(); call(ctx: TContext, command: string, arg?: any): Promise { @@ -28,8 +26,6 @@ export class ExtensionHostDebugBroadcastChannel implements IServerChan return Promise.resolve(this._onReloadEmitter.fire({ sessionId: arg[0] })); case 'terminate': return Promise.resolve(this._onTerminateEmitter.fire({ sessionId: arg[0] })); - case 'log': - return Promise.resolve(this._onLogToEmitter.fire({ sessionId: arg[0], log: arg[1] })); case 'attach': return Promise.resolve(this._onAttachEmitter.fire({ sessionId: arg[0], port: arg[1], subId: arg[2] })); } @@ -44,8 +40,6 @@ export class ExtensionHostDebugBroadcastChannel implements IServerChan return this._onReloadEmitter.event; case 'terminate': return this._onTerminateEmitter.event; - case 'log': - return this._onLogToEmitter.event; case 'attach': return this._onAttachEmitter.event; } @@ -85,14 +79,6 @@ export class ExtensionHostDebugChannelClient extends Disposable implements IExte return this.channel.listen('attach'); } - logToSession(sessionId: string, log: IRemoteConsoleLog): void { - this.channel.call('log', [sessionId, log]); - } - - get onLogToSession(): Event { - return this.channel.listen('log'); - } - terminateSession(sessionId: string, subId?: string): void { this.channel.call('terminate', [sessionId, subId]); } diff --git a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts index d05bd7ca0..f0ac255cb 100644 --- a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts @@ -8,8 +8,7 @@ import { IProcessEnvironment } from 'vs/base/common/platform'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { createServer, AddressInfo } from 'net'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; +import { IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; export class ElectronExtensionHostDebugBroadcastChannel extends ExtensionHostDebugBroadcastChannel { diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 2fe576f1b..2f2711440 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -314,10 +314,10 @@ export class DiagnosticsService implements IDiagnosticsService { if (isLinux) { systemInfo.linuxEnv = { - desktopSession: process.env.DESKTOP_SESSION, - xdgSessionDesktop: process.env.XDG_SESSION_DESKTOP, - xdgCurrentDesktop: process.env.XDG_CURRENT_DESKTOP, - xdgSessionType: process.env.XDG_SESSION_TYPE + desktopSession: process.env['DESKTOP_SESSION'], + xdgSessionDesktop: process.env['XDG_SESSION_DESKTOP'], + xdgCurrentDesktop: process.env['XDG_CURRENT_DESKTOP'], + xdgSessionType: process.env['XDG_SESSION_TYPE'] }; } diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 8f7bb194d..25e49ef1c 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -181,6 +181,7 @@ export interface IDialogOptions { cancelId?: number; detail?: string; checkbox?: ICheckbox; + useCustom?: boolean; } export interface IInput { diff --git a/src/vs/platform/dialogs/electron-main/dialogs.ts b/src/vs/platform/dialogs/electron-main/dialogMainService.ts similarity index 100% rename from src/vs/platform/dialogs/electron-main/dialogs.ts rename to src/vs/platform/dialogs/electron-main/dialogMainService.ts diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index e719e129c..019410e6a 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -28,6 +28,7 @@ export interface NativeParsedArgs { 'prof-startup'?: boolean; 'prof-startup-prefix'?: string; 'prof-append-timers'?: string; + 'prof-v8-extensions'?: boolean; verbose?: boolean; trace?: boolean; 'trace-category-filter'?: string; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 7263bcbf7..7b6a60b57 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -53,8 +53,8 @@ export const OPTIONS: OptionDescriptions> = { 'extensions-download-dir': { type: 'string' }, 'builtin-extensions-dir': { type: 'string' }, 'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") }, - 'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extension.") }, - 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extension.") }, + 'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extensions.") }, + 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions.") }, 'install-extension': { type: 'string[]', cat: 'e', args: 'extension-id[@version] | path-to-vsix', description: localize('installExtension', "Installs or updates the extension. The identifier of an extension is always `${publisher}.${name}`. Use `--force` argument to update to latest version. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.") }, 'uninstall-extension': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('uninstallExtension', "Uninstalls an extension.") }, 'enable-proposed-api': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") }, @@ -66,6 +66,7 @@ export const OPTIONS: OptionDescriptions> = { 'prof-startup': { type: 'boolean', cat: 't', description: localize('prof-startup', "Run CPU profiler during startup") }, 'prof-append-timers': { type: 'string' }, 'prof-startup-prefix': { type: 'string' }, + 'prof-v8-extensions': { type: 'boolean' }, 'disable-extensions': { type: 'boolean', deprecates: 'disableExtensions', cat: 't', description: localize('disableExtensions', "Disable all installed extensions.") }, 'disable-extension': { type: 'string[]', cat: 't', args: 'extension-id', description: localize('disableExtension', "Disable an extension.") }, 'sync': { type: 'string', cat: 't', description: localize('turn sync', "Turn sync on or off"), args: ['on', 'off'] }, @@ -149,10 +150,6 @@ export function parseArgs(args: string[], options: OptionDescriptions, err const string: string[] = []; const boolean: string[] = []; for (let optionId in options) { - if (optionId[0] === '_') { - continue; - } - const o = options[optionId]; if (o.alias) { alias[optionId] = o.alias; diff --git a/src/vs/platform/environment/test/node/environmentService.test.ts b/src/vs/platform/environment/test/node/environmentService.test.ts index d30dc8a68..ca33c2260 100644 --- a/src/vs/platform/environment/test/node/environmentService.test.ts +++ b/src/vs/platform/environment/test/node/environmentService.test.ts @@ -13,35 +13,35 @@ suite('EnvironmentService', () => { test('parseExtensionHostPort when built', () => { const parse = (a: string[]) => parseExtensionHostPort(parseArgs(a, OPTIONS), true); - assert.deepEqual(parse([]), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost']), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugBrkPluginHost']), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse([]), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost']), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost']), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); - assert.deepEqual(parse(['--inspect-extensions']), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-brk-extensions']), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse(['--inspect-extensions']), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions']), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); }); test('parseExtensionHostPort when unbuilt', () => { const parse = (a: string[]) => parseExtensionHostPort(parseArgs(a, OPTIONS), false); - assert.deepEqual(parse([]), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost']), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugBrkPluginHost']), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse([]), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost']), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost']), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); - assert.deepEqual(parse(['--inspect-extensions']), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-brk-extensions']), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse(['--inspect-extensions']), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions']), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); }); test('userDataPath', () => { @@ -57,10 +57,10 @@ suite('EnvironmentService', () => { test('careful with boolean file names', function () { let actual = parseArgs(['-r', 'arg.txt'], OPTIONS); assert(actual['reuse-window']); - assert.deepEqual(actual._, ['arg.txt']); + assert.deepStrictEqual(actual._, ['arg.txt']); actual = parseArgs(['-r', 'true.txt'], OPTIONS); assert(actual['reuse-window']); - assert.deepEqual(actual._, ['true.txt']); + assert.deepStrictEqual(actual._, ['true.txt']); }); }); diff --git a/src/vs/platform/environment/test/node/nativeModules.test.ts b/src/vs/platform/environment/test/node/nativeModules.test.ts index b64282f62..28013dfcd 100644 --- a/src/vs/platform/environment/test/node/nativeModules.test.ts +++ b/src/vs/platform/environment/test/node/nativeModules.test.ts @@ -37,11 +37,6 @@ suite('Native Modules (all platforms)', () => { assert.ok(typeof spdlog.createRotatingLogger === 'function', testErrorMessage('spdlog')); }); - test('v8-inspect-profiler', async () => { - const profiler = await import('v8-inspect-profiler'); - assert.ok(typeof profiler.startProfiling === 'function', testErrorMessage('v8-inspect-profiler')); - }); - test('vscode-nsfw', async () => { const nsfWatcher = await import('vscode-nsfw'); assert.ok(typeof nsfWatcher === 'function', testErrorMessage('vscode-nsfw')); @@ -90,7 +85,7 @@ suite('Native Modules (all platforms)', () => { test('vscode-windows-ca-certs', async () => { // @ts-ignore Windows only const windowsCerts = await import('vscode-windows-ca-certs'); - const store = windowsCerts(); + const store = new windowsCerts.Crypt32(); assert.ok(windowsCerts, testErrorMessage('vscode-windows-ca-certs')); let certCount = 0; try { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 3f0dd835c..45d3b68c4 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -9,7 +9,7 @@ import { getGalleryExtensionId, getGalleryExtensionTelemetryData, adoptToGallery import { getOrDefault } from 'vs/base/common/objects'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IPager } from 'vs/base/common/paging'; -import { IRequestService, asJson, asText } from 'vs/platform/request/common/request'; +import { IRequestService, asJson, asText, isSuccess } from 'vs/platform/request/common/request'; import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -147,6 +147,35 @@ const DefaultQueryState: IQueryState = { assetTypes: [] }; +type GalleryServiceQueryClassification = { + filterTypes: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + sortBy: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + sortOrder: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + duration: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', 'isMeasurement': true }; + success: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + requestBodySize: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + responseBodySize?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + statusCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + errorCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + count?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; +}; + +type QueryTelemetryData = { + filterTypes: string[]; + sortBy: string; + sortOrder: string; +}; + +type GalleryServiceQueryEvent = QueryTelemetryData & { + duration: number; + success: boolean; + requestBodySize: string; + responseBodySize?: string; + statusCode?: string; + errorCode?: string; + count?: string; +}; + class Query { constructor(private state = DefaultQueryState) { } @@ -196,6 +225,14 @@ class Query { const criterium = this.state.criteria.filter(criterium => criterium.filterType === FilterType.SearchText)[0]; return criterium && criterium.value ? criterium.value : ''; } + + get telemetryData(): QueryTelemetryData { + return { + filterTypes: this.state.criteria.map(criterium => String(criterium.filterType)), + sortBy: String(this.sortBy), + sortOrder: String(this.sortOrder) + }; + } } function getStatistic(statistics: IRawGalleryExtensionStatistics[], name: string): number { @@ -447,20 +484,9 @@ export class ExtensionGalleryService implements IExtensionGalleryService { throw new Error('No extension gallery service configured.'); } - const type = options.names ? 'ids' : (options.text ? 'text' : 'all'); let text = options.text || ''; const pageSize = getOrDefault(options, o => o.pageSize, 50); - type GalleryServiceQueryClassification = { - type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - text: { classification: 'CustomerContent', purpose: 'FeatureInsight' }; - }; - type GalleryServiceQueryEvent = { - type: string; - text: string; - }; - this.telemetryService.publicLog2('galleryService:query', { type, text }); - let query = new Query() .withFlags(Flags.IncludeLatestVersionOnly, Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties) .withPage(1, pageSize) @@ -543,27 +569,49 @@ export class ExtensionGalleryService implements IExtensionGalleryService { 'Content-Length': String(data.length) }; - const context = await this.requestService.request({ - type: 'POST', - url: this.api('/extensionquery'), - data, - headers - }, token); + const startTime = new Date().getTime(); + let context: IRequestContext | undefined, error: any, total: number = 0; - if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) { - return { galleryExtensions: [], total: 0 }; + try { + context = await this.requestService.request({ + type: 'POST', + url: this.api('/extensionquery'), + data, + headers + }, token); + + if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) { + return { galleryExtensions: [], total }; + } + + const result = await asJson(context); + if (result) { + const r = result.results[0]; + const galleryExtensions = r.extensions; + const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0]; + total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0; + + return { galleryExtensions, total }; + } + return { galleryExtensions: [], total }; + + } catch (e) { + error = e; + throw e; + } finally { + this.telemetryService.publicLog2('galleryService:query', { + ...query.telemetryData, + requestBodySize: String(data.length), + duration: new Date().getTime() - startTime, + success: !!context && isSuccess(context), + responseBodySize: context?.res.headers['Content-Length'], + statusCode: context ? String(context.res.statusCode) : undefined, + errorCode: error + ? isPromiseCanceledError(error) ? 'canceled' : getErrorMessage(error).startsWith('XHR timeout') ? 'timeout' : 'failed' + : undefined, + count: String(total) + }); } - - const result = await asJson(context); - if (result) { - const r = result.results[0]; - const galleryExtensions = r.extensions; - const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0]; - const total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0; - - return { galleryExtensions, total }; - } - return { galleryExtensions: [], total: 0 }; } async reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 0e46a92d6..50531800e 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -278,3 +278,19 @@ export const ExtensionsLocalizedLabel = { value: ExtensionsLabel, original: 'Ext export const ExtensionsChannelId = 'extensions'; export const PreferencesLabel = localize('preferences', "Preferences"); export const PreferencesLocalizedLabel = { value: PreferencesLabel, original: 'Preferences' }; + + +export interface CLIOutput { + log(s: string): void; + error(s: string): void; +} + +export const IExtensionManagementCLIService = createDecorator('IExtensionManagementCLIService'); +export interface IExtensionManagementCLIService { + readonly _serviceBrand: undefined; + + listExtensions(showVersions: boolean, category?: string, output?: CLIOutput): Promise; + installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output?: CLIOutput): Promise; + uninstallExtensions(extensions: (string | URI)[], force: boolean, output?: CLIOutput): Promise; + locateExtension(extensions: string[], output?: CLIOutput): Promise; +} diff --git a/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts b/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts new file mode 100644 index 000000000..532a550c5 --- /dev/null +++ b/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts @@ -0,0 +1,347 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; + +import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; +import { gt } from 'vs/base/common/semver/semver'; +import { CLIOutput, IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; +import { getBaseLabel } from 'vs/base/common/labels'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Schemas } from 'vs/base/common/network'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; + +const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id); +const useId = localize('useId', "Make sure you use the full extension ID, including the publisher, e.g.: {0}", 'ms-dotnettools.csharp'); + + +function getId(manifest: IExtensionManifest, withVersion?: boolean): string { + if (withVersion) { + return `${manifest.publisher}.${manifest.name}@${manifest.version}`; + } else { + return `${manifest.publisher}.${manifest.name}`; + } +} + +const EXTENSION_ID_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/; + +export function getIdAndVersion(id: string): [string, string | undefined] { + const matches = EXTENSION_ID_REGEX.exec(id); + if (matches && matches[1]) { + return [adoptToGalleryExtensionId(matches[1]), matches[2]]; + } + return [adoptToGalleryExtensionId(id), undefined]; +} + +type InstallExtensionInfo = { id: string, version?: string, installOptions: InstallOptions }; + + +export class ExtensionManagementCLIService implements IExtensionManagementCLIService { + + _serviceBrand: any; + + constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, + @ILocalizationsService private readonly localizationsService: ILocalizationsService + ) { } + + protected get location(): string | undefined { + return undefined; + } + + public async listExtensions(showVersions: boolean, category?: string, output: CLIOutput = console): Promise { + let extensions = await this.extensionManagementService.getInstalled(ExtensionType.User); + const categories = EXTENSION_CATEGORIES.map(c => c.toLowerCase()); + if (category && category !== '') { + if (categories.indexOf(category.toLowerCase()) < 0) { + output.log('Invalid category please enter a valid category. To list valid categories run --category without a category specified'); + return; + } + extensions = extensions.filter(e => { + if (e.manifest.categories) { + const lowerCaseCategories: string[] = e.manifest.categories.map(c => c.toLowerCase()); + return lowerCaseCategories.indexOf(category.toLowerCase()) > -1; + } + return false; + }); + } else if (category === '') { + output.log('Possible Categories: '); + categories.forEach(category => { + output.log(category); + }); + return; + } + if (this.location) { + output.log(localize('listFromLocation', "Extensions installed on {0}:", this.location)); + } + + extensions = extensions.sort((e1, e2) => e1.identifier.id.localeCompare(e2.identifier.id)); + let lastId: string | undefined = undefined; + for (let extension of extensions) { + if (lastId !== extension.identifier.id) { + lastId = extension.identifier.id; + output.log(getId(extension.manifest, showVersions)); + } + } + } + + public async installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output: CLIOutput = console): Promise { + const failed: string[] = []; + const installedExtensionsManifests: IExtensionManifest[] = []; + if (extensions.length) { + output.log(this.location ? localize('installingExtensionsOnLocation', "Installing extensions on {0}...", this.location) : localize('installingExtensions', "Installing extensions...")); + } + + const installed = await this.extensionManagementService.getInstalled(ExtensionType.User); + const checkIfNotInstalled = (id: string, version?: string): boolean => { + const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id })); + if (installedExtension) { + if (!version && !force) { + output.log(localize('alreadyInstalled-checkAndUpdate', "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@' to install a specific version, for example: '{2}@1.2.3'.", id, installedExtension.manifest.version, id)); + return false; + } + if (version && installedExtension.manifest.version === version) { + output.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", `${id}@${version}`)); + return false; + } + } + return true; + }; + const vsixs: URI[] = []; + const installExtensionInfos: InstallExtensionInfo[] = []; + for (const extension of extensions) { + if (extension instanceof URI) { + vsixs.push(extension); + } else { + const [id, version] = getIdAndVersion(extension); + if (checkIfNotInstalled(id, version)) { + installExtensionInfos.push({ id, version, installOptions: { isBuiltin: false, isMachineScoped } }); + } + } + } + for (const extension of builtinExtensionIds) { + const [id, version] = getIdAndVersion(extension); + if (checkIfNotInstalled(id, version)) { + installExtensionInfos.push({ id, version, installOptions: { isBuiltin: true, isMachineScoped: false } }); + } + } + + if (vsixs.length) { + await Promise.all(vsixs.map(async vsix => { + try { + const manifest = await this.installVSIX(vsix, force, output); + if (manifest) { + installedExtensionsManifests.push(manifest); + } + } catch (err) { + output.error(err.message || err.stack || err); + failed.push(vsix.toString()); + } + })); + } + + if (installExtensionInfos.length) { + + const galleryExtensions = await this.getGalleryExtensions(installExtensionInfos); + + await Promise.all(installExtensionInfos.map(async extensionInfo => { + const gallery = galleryExtensions.get(extensionInfo.id.toLowerCase()); + if (gallery) { + try { + const manifest = await this.installFromGallery(extensionInfo, gallery, installed, force, output); + if (manifest) { + installedExtensionsManifests.push(manifest); + } + } catch (err) { + output.error(err.message || err.stack || err); + failed.push(extensionInfo.id); + } + } else { + output.error(`${notFound(extensionInfo.version ? `${extensionInfo.id}@${extensionInfo.version}` : extensionInfo.id)}\n${useId}`); + failed.push(extensionInfo.id); + } + })); + + } + + if (installedExtensionsManifests.some(manifest => isLanguagePackExtension(manifest))) { + await this.updateLocalizationsCache(); + } + + if (failed.length) { + throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', '))); + } + } + + private async installVSIX(vsix: URI, force: boolean, output: CLIOutput): Promise { + + const manifest = await this.extensionManagementService.getManifest(vsix); + if (!manifest) { + throw new Error('Invalid vsix'); + } + + const valid = await this.validateVSIX(manifest, force, output); + if (valid) { + try { + await this.extensionManagementService.install(vsix); + output.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(vsix))); + return manifest; + } catch (error) { + if (isPromiseCanceledError(error)) { + output.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", getBaseLabel(vsix))); + return null; + } else { + throw error; + } + } + } + return null; + } + + private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise> { + const extensionIds = extensions.filter(({ version }) => version === undefined).map(({ id }) => id); + const extensionsWithIdAndVersion = extensions.filter(({ version }) => version !== undefined); + + const galleryExtensions = new Map(); + await Promise.all([ + (async () => { + const result = await this.extensionGalleryService.getExtensions(extensionIds, CancellationToken.None); + result.forEach(extension => galleryExtensions.set(extension.identifier.id.toLowerCase(), extension)); + })(), + Promise.all(extensionsWithIdAndVersion.map(async ({ id, version }) => { + const extension = await this.extensionGalleryService.getCompatibleExtension({ id }, version); + if (extension) { + galleryExtensions.set(extension.identifier.id.toLowerCase(), extension); + } + })) + ]); + + return galleryExtensions; + } + + private async installFromGallery({ id, version, installOptions }: InstallExtensionInfo, galleryExtension: IGalleryExtension, installed: ILocalExtension[], force: boolean, output: CLIOutput): Promise { + const manifest = await this.extensionGalleryService.getManifest(galleryExtension, CancellationToken.None); + if (manifest && !this.validateExtensionKind(manifest, output)) { + return null; + } + + const installedExtension = installed.find(e => areSameExtensions(e.identifier, galleryExtension.identifier)); + if (installedExtension) { + if (galleryExtension.version === installedExtension.manifest.version) { + output.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", version ? `${id}@${version}` : id)); + return null; + } + output.log(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, galleryExtension.version)); + } + + try { + if (installOptions.isBuiltin) { + output.log(localize('installing builtin ', "Installing builtin extension '{0}' v{1}...", id, galleryExtension.version)); + } else { + output.log(localize('installing', "Installing extension '{0}' v{1}...", id, galleryExtension.version)); + } + + await this.extensionManagementService.installFromGallery(galleryExtension, installOptions); + output.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, galleryExtension.version)); + return manifest; + } catch (error) { + if (isPromiseCanceledError(error)) { + output.log(localize('cancelInstall', "Cancelled installing extension '{0}'.", id)); + return null; + } else { + throw error; + } + } + } + + protected validateExtensionKind(_manifest: IExtensionManifest, output: CLIOutput): boolean { + return true; + } + + private async validateVSIX(manifest: IExtensionManifest, force: boolean, output: CLIOutput): Promise { + const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; + const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); + const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && gt(local.manifest.version, manifest.version)); + + if (newer && !force) { + output.log(localize('forceDowngrade', "A newer version of extension '{0}' v{1} is already installed. Use '--force' option to downgrade to older version.", newer.identifier.id, newer.manifest.version, manifest.version)); + return false; + } + + return this.validateExtensionKind(manifest, output); + } + + public async uninstallExtensions(extensions: (string | URI)[], force: boolean, output: CLIOutput = console): Promise { + const getExtensionId = async (extensionDescription: string | URI): Promise => { + if (extensionDescription instanceof URI) { + const manifest = await this.extensionManagementService.getManifest(extensionDescription); + return getId(manifest); + } + return extensionDescription; + }; + + const uninstalledExtensions: ILocalExtension[] = []; + for (const extension of extensions) { + const id = await getExtensionId(extension); + const installed = await this.extensionManagementService.getInstalled(); + const extensionsToUninstall = installed.filter(e => areSameExtensions(e.identifier, { id })); + if (!extensionsToUninstall.length) { + throw new Error(`${this.notInstalled(id)}\n${useId}`); + } + if (extensionsToUninstall.some(e => e.type === ExtensionType.System)) { + output.log(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be uninstalled", id)); + return; + } + if (!force && extensionsToUninstall.some(e => e.isBuiltin)) { + output.log(localize('forceUninstall', "Extension '{0}' is marked as a Built-in extension by user. Please use '--force' option to uninstall it.", id)); + return; + } + output.log(localize('uninstalling', "Uninstalling {0}...", id)); + for (const extensionToUninstall of extensionsToUninstall) { + await this.extensionManagementService.uninstall(extensionToUninstall); + uninstalledExtensions.push(extensionToUninstall); + } + + if (this.location) { + output.log(localize('successUninstallFromLocation', "Extension '{0}' was successfully uninstalled from {1}!", id, this.location)); + } else { + output.log(localize('successUninstall', "Extension '{0}' was successfully uninstalled!", id)); + } + + } + + if (uninstalledExtensions.some(e => isLanguagePackExtension(e.manifest))) { + await this.updateLocalizationsCache(); + } + } + + public async locateExtension(extensions: string[], output: CLIOutput = console): Promise { + const installed = await this.extensionManagementService.getInstalled(); + extensions.forEach(e => { + installed.forEach(i => { + if (i.identifier.id === e) { + if (i.location.scheme === Schemas.file) { + output.log(i.location.fsPath); + return; + } + } + }); + }); + } + + + private updateLocalizationsCache(): Promise { + return this.localizationsService.update(); + } + + private notInstalled(id: string) { + return this.location ? localize('notInstalleddOnLocation', "Extension '{0}' is not installed on {1}.", id, this.location) : localize('notInstalled', "Extension '{0}' is not installed.", id); + } + +} diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index 20d24771d..393745571 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -42,12 +42,16 @@ export class ExtensionIdentifierWithVersion implements IExtensionIdentifierWithV } } +export function getExtensionId(publisher: string, name: string): string { + return `${publisher}.${name}`; +} + export function adoptToGalleryExtensionId(id: string): string { return id.toLocaleLowerCase(); } export function getGalleryExtensionId(publisher: string, name: string): string { - return `${publisher.toLocaleLowerCase()}.${name.toLocaleLowerCase()}`; + return adoptToGalleryExtensionId(getExtensionId(publisher, name)); } export function groupByExtension(extensions: T[], getExtensionIdentifier: (t: T) => IExtensionIdentifier): T[][] { diff --git a/src/vs/platform/extensionManagement/electron-sandbox/extensionTipsService.ts b/src/vs/platform/extensionManagement/electron-sandbox/extensionTipsService.ts index 7d17d9ef3..a9ec2f35a 100644 --- a/src/vs/platform/extensionManagement/electron-sandbox/extensionTipsService.ts +++ b/src/vs/platform/extensionManagement/electron-sandbox/extensionTipsService.ts @@ -21,6 +21,8 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { localize } from 'vs/nls'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Event } from 'vs/base/common/event'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; type ExeExtensionRecommendationsClassification = { extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; @@ -52,6 +54,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService { @ITelemetryService private readonly telemetryService: ITelemetryService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IStorageService private readonly storageService: IStorageService, + @INativeHostService private readonly nativeHostService: INativeHostService, @IExtensionRecommendationNotificationService private readonly extensionRecommendationNotificationService: IExtensionRecommendationNotificationService, @IFileService fileService: IFileService, @IProductService productService: IProductService, @@ -172,6 +175,11 @@ export class ExtensionTipsService extends BaseExtensionTipsService { case RecommendationsNotificationResult.Ignored: this.highImportanceTipsByExe.delete(exeName); break; + case RecommendationsNotificationResult.IncompatibleWindow: + // Recommended in incompatible window. Schedule the prompt after active window change + const onActiveWindowChange = Event.once(Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow))); + this._register(onActiveWindowChange(() => this.promptHighImportanceExeBasedTip())); + break; case RecommendationsNotificationResult.TooMany: // Too many notifications. Schedule the prompt after one hour const disposable = this._register(disposableTimeout(() => { disposable.dispose(); this.promptHighImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */)); @@ -217,6 +225,12 @@ export class ExtensionTipsService extends BaseExtensionTipsService { this.promptMediumImportanceExeBasedTip(); break; + case RecommendationsNotificationResult.IncompatibleWindow: + // Recommended in incompatible window. Schedule the prompt after active window change + const onActiveWindowChange = Event.once(Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow))); + this._register(onActiveWindowChange(() => this.promptMediumImportanceExeBasedTip())); + break; + case RecommendationsNotificationResult.TooMany: // Too many notifications. Schedule the prompt after one hour const disposable2 = this._register(disposableTimeout(() => { disposable2.dispose(); this.promptMediumImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */)); diff --git a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts new file mode 100644 index 000000000..c1085576c --- /dev/null +++ b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { isUUID } from 'vs/base/common/uuid'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { NullLogService } from 'vs/platform/log/common/log'; +import product from 'vs/platform/product/common/product'; +import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { URI } from 'vs/base/common/uri'; +import { joinPath } from 'vs/base/common/resources'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { mock } from 'vs/base/test/common/mock'; + +class EnvironmentServiceMock extends mock() { + constructor(readonly serviceMachineIdResource: URI) { + super(); + } +} + +suite('Extension Gallery Service', () => { + const disposables: DisposableStore = new DisposableStore(); + let fileService: IFileService, environmentService: IEnvironmentService, storageService: IStorageService; + + setup(() => { + const serviceMachineIdResource = joinPath(URI.file('tests').with({ scheme: 'vscode-tests' }), 'machineid'); + environmentService = new EnvironmentServiceMock(serviceMachineIdResource); + fileService = disposables.add(new FileService(new NullLogService())); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + fileService.registerProvider(serviceMachineIdResource.scheme, fileSystemProvider); + storageService = new InMemoryStorageService(); + }); + + teardown(() => disposables.clear()); + + test('marketplace machine id', async () => { + const headers = await resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService); + assert.ok(isUUID(headers['X-Market-User-Id'])); + const headers2 = await resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService); + assert.equal(headers['X-Market-User-Id'], headers2['X-Market-User-Id']); + }); +}); diff --git a/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts deleted file mode 100644 index e2a7ed43a..000000000 --- a/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import * as os from 'os'; -import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; -import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { join } from 'vs/base/common/path'; -import { mkdirp, RimRafMode, rimraf } from 'vs/base/node/pfs'; -import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService'; -import { isUUID } from 'vs/base/common/uuid'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IFileService } from 'vs/platform/files/common/files'; -import { FileService } from 'vs/platform/files/common/fileService'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; -import { Schemas } from 'vs/base/common/network'; -import product from 'vs/platform/product/common/product'; -import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; -import { IStorageService } from 'vs/platform/storage/common/storage'; - -suite('Extension Gallery Service', () => { - const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'extensiongalleryservice'); - const marketplaceHome = join(parentDir, 'Marketplace'); - let fileService: IFileService; - let disposables: DisposableStore; - - setup(done => { - - disposables = new DisposableStore(); - fileService = new FileService(new NullLogService()); - disposables.add(fileService); - - const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); - disposables.add(diskFileSystemProvider); - fileService.registerProvider(Schemas.file, diskFileSystemProvider); - - // Delete any existing backups completely and then re-create it. - rimraf(marketplaceHome, RimRafMode.MOVE).then(() => { - mkdirp(marketplaceHome).then(() => { - done(); - }, error => done(error)); - }, error => done(error)); - }); - - teardown(done => { - disposables.clear(); - rimraf(marketplaceHome, RimRafMode.MOVE).then(done, done); - }); - - test('marketplace machine id', () => { - const args = ['--user-data-dir', marketplaceHome]; - const environmentService = new NativeEnvironmentService(parseArgs(args, OPTIONS)); - const storageService: IStorageService = new TestStorageService(); - - return resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService).then(headers => { - assert.ok(isUUID(headers['X-Market-User-Id'])); - - return resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService).then(headers2 => { - assert.equal(headers['X-Market-User-Id'], headers2['X-Market-User-Id']); - }); - }); - }); -}); diff --git a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts index cea1c09e2..c3eff4b7a 100644 --- a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts +++ b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts @@ -11,10 +11,19 @@ export const enum RecommendationSource { EXE = 3 } +export function RecommendationSourceToString(source: RecommendationSource) { + switch (source) { + case RecommendationSource.FILE: return 'file'; + case RecommendationSource.WORKSPACE: return 'workspace'; + case RecommendationSource.EXE: return 'exe'; + } +} + export const enum RecommendationsNotificationResult { Ignored = 'ignored', Cancelled = 'cancelled', TooMany = 'toomany', + IncompatibleWindow = 'incompatibleWindow', Accepted = 'reacted', } diff --git a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts index b5bdc6235..c863c64bf 100644 --- a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts +++ b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts @@ -8,14 +8,24 @@ import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapab import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { VSBuffer } from 'vs/base/common/buffer'; -import { joinPath, extUri, dirname } from 'vs/base/common/resources'; +import { Throttler } from 'vs/base/common/async'; import { localize } from 'vs/nls'; import * as browser from 'vs/base/browser/browser'; +import { joinPath } from 'vs/base/common/resources'; const INDEXEDDB_VSCODE_DB = 'vscode-web-db'; export const INDEXEDDB_USERDATA_OBJECT_STORE = 'vscode-userdata-store'; export const INDEXEDDB_LOGS_OBJECT_STORE = 'vscode-logs-store'; +// Standard FS Errors (expected to be thrown in production when invalid FS operations are requested) +const ERR_FILE_NOT_FOUND = createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound); +const ERR_FILE_IS_DIR = createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory); +const ERR_FILE_NOT_DIR = createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory); +const ERR_DIR_NOT_EMPTY = createFileSystemProviderError(localize('dirIsNotEmpty', "Directory is not empty"), FileSystemProviderErrorCode.Unknown); + +// Arbitrary Internal Errors (should never be thrown in production) +const ERR_UNKNOWN_INTERNAL = (message: string) => createFileSystemProviderError(localize('internal', "Internal error occured in IndexedDB File System Provider. ({0})", message), FileSystemProviderErrorCode.Unknown); + export class IndexedDB { private indexedDBPromise: Promise; @@ -38,7 +48,7 @@ export class IndexedDB { } private openIndexedDB(name: string, version: number, stores: string[]): Promise { - if (browser.isEdge) { + if (browser.isEdgeLegacy) { return Promise.resolve(null); } return new Promise((c, e) => { @@ -65,13 +75,140 @@ export class IndexedDB { }; }); } - } export interface IIndexedDBFileSystemProvider extends Disposable, IFileSystemProviderWithFileReadWriteCapability { reset(): Promise; } +type DirEntry = [string, FileType]; + +type IndexedDBFileSystemEntry = + | { + path: string, + type: FileType.Directory, + children: Map, + } + | { + path: string, + type: FileType.File, + size: number | undefined, + }; + +class IndexedDBFileSystemNode { + public type: FileType; + + constructor(private entry: IndexedDBFileSystemEntry) { + this.type = entry.type; + } + + + read(path: string) { + return this.doRead(path.split('/').filter(p => p.length)); + } + + private doRead(pathParts: string[]): IndexedDBFileSystemEntry | undefined { + if (pathParts.length === 0) { return this.entry; } + if (this.entry.type !== FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL('Internal error reading from IndexedDBFSNode -- expected directory at ' + this.entry.path); + } + const next = this.entry.children.get(pathParts[0]); + + if (!next) { return undefined; } + return next.doRead(pathParts.slice(1)); + } + + delete(path: string) { + const toDelete = path.split('/').filter(p => p.length); + if (toDelete.length === 0) { + if (this.entry.type !== FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL(`Internal error deleting from IndexedDBFSNode. Expected root entry to be directory`); + } + this.entry.children.clear(); + } else { + return this.doDelete(toDelete, path); + } + } + + private doDelete = (pathParts: string[], originalPath: string) => { + if (pathParts.length === 0) { + throw ERR_UNKNOWN_INTERNAL(`Internal error deleting from IndexedDBFSNode -- got no deletion path parts (encountered while deleting ${originalPath})`); + } + else if (this.entry.type !== FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL('Internal error deleting from IndexedDBFSNode -- expected directory at ' + this.entry.path); + } + else if (pathParts.length === 1) { + this.entry.children.delete(pathParts[0]); + } + else { + const next = this.entry.children.get(pathParts[0]); + if (!next) { + throw ERR_UNKNOWN_INTERNAL('Internal error deleting from IndexedDBFSNode -- expected entry at ' + this.entry.path + '/' + next); + } + next.doDelete(pathParts.slice(1), originalPath); + } + }; + + add(path: string, entry: { type: 'file', size?: number } | { type: 'dir' }) { + this.doAdd(path.split('/').filter(p => p.length), entry, path); + } + + private doAdd(pathParts: string[], entry: { type: 'file', size?: number } | { type: 'dir' }, originalPath: string) { + if (pathParts.length === 0) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- adding empty path (encountered while adding ${originalPath})`); + } + else if (this.entry.type !== FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- parent is not a directory (encountered while adding ${originalPath})`); + } + else if (pathParts.length === 1) { + const next = pathParts[0]; + const existing = this.entry.children.get(next); + if (entry.type === 'dir') { + if (existing?.entry.type === FileType.File) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting file with directory: ${this.entry.path}/${next} (encountered while adding ${originalPath})`); + } + this.entry.children.set(next, existing ?? new IndexedDBFileSystemNode({ + type: FileType.Directory, + path: this.entry.path + '/' + next, + children: new Map(), + })); + } else { + if (existing?.entry.type === FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting directory with file: ${this.entry.path}/${next} (encountered while adding ${originalPath})`); + } + this.entry.children.set(next, new IndexedDBFileSystemNode({ + type: FileType.File, + path: this.entry.path + '/' + next, + size: entry.size, + })); + } + } + else if (pathParts.length > 1) { + const next = pathParts[0]; + let childNode = this.entry.children.get(next); + if (!childNode) { + childNode = new IndexedDBFileSystemNode({ + children: new Map(), + path: this.entry.path + '/' + next, + type: FileType.Directory + }); + this.entry.children.set(next, childNode); + } + else if (childNode.type === FileType.File) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting file entry with directory: ${this.entry.path}/${next} (encountered while adding ${originalPath})`); + } + childNode.doAdd(pathParts.slice(1), entry, originalPath); + } + } + + print(indentation = '') { + console.log(indentation + this.entry.path); + if (this.entry.type === FileType.Directory) { + this.entry.children.forEach(child => child.print(indentation + ' ')); + } + } +} + class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSystemProvider { readonly capabilities: FileSystemProviderCapabilities = @@ -83,11 +220,14 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy readonly onDidChangeFile: Event = this._onDidChangeFile.event; private readonly versions: Map = new Map(); - private readonly dirs: Set = new Set(); - constructor(private readonly scheme: string, private readonly database: IDBDatabase, private readonly store: string) { + private cachedFiletree: Promise | undefined; + private writeManyThrottler: Throttler; + + constructor(scheme: string, private readonly database: IDBDatabase, private readonly store: string) { super(); - this.dirs.add('/'); + this.writeManyThrottler = new Throttler(); + } watch(resource: URI, opts: IWatchOptions): IDisposable { @@ -98,29 +238,22 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy try { const resourceStat = await this.stat(resource); if (resourceStat.type === FileType.File) { - throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory); + throw ERR_FILE_NOT_DIR; } } catch (error) { /* Ignore */ } - - // Make sure parent dir exists - await this.stat(dirname(resource)); - - this.dirs.add(resource.path); + (await this.getFiletree()).add(resource.path, { type: 'dir' }); } async stat(resource: URI): Promise { - try { - const content = await this.readFile(resource); + const content = (await this.getFiletree()).read(resource.path); + if (content?.type === FileType.File) { return { type: FileType.File, ctime: 0, mtime: this.versions.get(resource.toString()) || 0, - size: content.byteLength + size: content.size ?? (await this.readFile(resource)).byteLength }; - } catch (e) { - } - const files = await this.readdir(resource); - if (files.length) { + } else if (content?.type === FileType.Directory) { return { type: FileType.Directory, ctime: 0, @@ -128,75 +261,112 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy size: 0 }; } - if (this.dirs.has(resource.path)) { - return { - type: FileType.Directory, - ctime: 0, - mtime: 0, - size: 0 - }; + else { + throw ERR_FILE_NOT_FOUND; } - throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound); } - async readdir(resource: URI): Promise<[string, FileType][]> { - const hasKey = await this.hasKey(resource.path); - if (hasKey) { - throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory); + async readdir(resource: URI): Promise { + const entry = (await this.getFiletree()).read(resource.path); + if (!entry) { + // Dirs aren't saved to disk, so empty dirs will be lost on reload. + // Thus we have two options for what happens when you try to read a dir and nothing is found: + // - Throw FileSystemProviderErrorCode.FileNotFound + // - Return [] + // We choose to return [] as creating a dir then reading it (even after reload) should not throw an error. + return []; } - const keys = await this.getAllKeys(); - const files: Map = new Map(); - for (const key of keys) { - const keyResource = this.toResource(key); - if (extUri.isEqualOrParent(keyResource, resource)) { - const path = extUri.relativePath(resource, keyResource); - if (path) { - const keySegments = path.split('/'); - files.set(keySegments[0], [keySegments[0], keySegments.length === 1 ? FileType.File : FileType.Directory]); - } - } + if (entry.type !== FileType.Directory) { + throw ERR_FILE_NOT_DIR; + } + else { + return [...entry.children.entries()].map(([name, node]) => [name, node.type]); } - return [...files.values()]; } async readFile(resource: URI): Promise { - const hasKey = await this.hasKey(resource.path); - if (!hasKey) { - throw createFileSystemProviderError(localize('fileNotFound', "File not found"), FileSystemProviderErrorCode.FileNotFound); - } - const value = await this.getValue(resource.path); - if (typeof value === 'string') { - return VSBuffer.fromString(value).buffer; - } else { - return value; - } + const buffer = await new Promise((c, e) => { + const transaction = this.database.transaction([this.store]); + const objectStore = transaction.objectStore(this.store); + const request = objectStore.get(resource.path); + request.onerror = () => e(request.error); + request.onsuccess = () => { + if (request.result instanceof Uint8Array) { + c(request.result); + } else if (typeof request.result === 'string') { + c(VSBuffer.fromString(request.result).buffer); + } + else { + if (request.result === undefined) { + e(ERR_FILE_NOT_FOUND); + } else { + e(ERR_UNKNOWN_INTERNAL(`IndexedDB entry at "${resource.path}" in unexpected format`)); + } + } + }; + }); + + (await this.getFiletree()).add(resource.path, { type: 'file', size: buffer.byteLength }); + return buffer; } async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { - const hasKey = await this.hasKey(resource.path); - if (!hasKey) { - const files = await this.readdir(resource); - if (files.length) { - throw createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory); - } + const existing = await this.stat(resource).catch(() => undefined); + if (existing?.type === FileType.Directory) { + throw ERR_FILE_IS_DIR; } - await this.setValue(resource.path, content); + + this.fileWriteBatch.push({ content, resource }); + await this.writeManyThrottler.queue(() => this.writeMany()); + (await this.getFiletree()).add(resource.path, { type: 'file', size: content.byteLength }); this.versions.set(resource.toString(), (this.versions.get(resource.toString()) || 0) + 1); this._onDidChangeFile.fire([{ resource, type: FileChangeType.UPDATED }]); } async delete(resource: URI, opts: FileDeleteOptions): Promise { - const hasKey = await this.hasKey(resource.path); - if (hasKey) { - await this.deleteKey(resource.path); - this.versions.delete(resource.path); - this._onDidChangeFile.fire([{ resource, type: FileChangeType.DELETED }]); - return; + let stat: IStat; + try { + stat = await this.stat(resource); + } catch (e) { + if (e.code === FileSystemProviderErrorCode.FileNotFound) { + return; + } + throw e; } + let toDelete: string[]; if (opts.recursive) { - const files = await this.readdir(resource); - await Promise.all(files.map(([key]) => this.delete(joinPath(resource, key), opts))); + const tree = (await this.tree(resource)); + toDelete = tree.map(([path]) => path); + } else { + if (stat.type === FileType.Directory && (await this.readdir(resource)).length) { + throw ERR_DIR_NOT_EMPTY; + } + toDelete = [resource.path]; + } + await this.deleteKeys(toDelete); + (await this.getFiletree()).delete(resource.path); + toDelete.forEach(key => this.versions.delete(key)); + this._onDidChangeFile.fire(toDelete.map(path => ({ resource: resource.with({ path }), type: FileChangeType.DELETED }))); + } + + private async tree(resource: URI): Promise { + if ((await this.stat(resource)).type === FileType.Directory) { + const topLevelEntries = (await this.readdir(resource)).map(([key, type]) => { + return [joinPath(resource, key).path, type] as [string, FileType]; + }); + let allEntries = topLevelEntries; + await Promise.all(topLevelEntries.map( + async ([key, type]) => { + if (type === FileType.Directory) { + const childEntries = (await this.tree(resource.with({ path: key }))); + allEntries = allEntries.concat(childEntries); + } + })); + return allEntries; + } else { + const entries: DirEntry[] = [[resource.path, FileType.File]]; + return entries; } } @@ -204,58 +374,57 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy return Promise.reject(new Error('Not Supported')); } - private toResource(key: string): URI { - return URI.file(key).with({ scheme: this.scheme }); + private getFiletree(): Promise { + if (!this.cachedFiletree) { + this.cachedFiletree = new Promise((c, e) => { + const transaction = this.database.transaction([this.store]); + const objectStore = transaction.objectStore(this.store); + const request = objectStore.getAllKeys(); + request.onerror = () => e(request.error); + request.onsuccess = () => { + const rootNode = new IndexedDBFileSystemNode({ + children: new Map(), + path: '', + type: FileType.Directory + }); + const keys = request.result.map(key => key.toString()); + keys.forEach(key => rootNode.add(key, { type: 'file' })); + c(rootNode); + }; + }); + } + return this.cachedFiletree; } - async getAllKeys(): Promise { - return new Promise(async (c, e) => { - const transaction = this.database.transaction([this.store]); - const objectStore = transaction.objectStore(this.store); - const request = objectStore.getAllKeys(); - request.onerror = () => e(request.error); - request.onsuccess = () => c(request.result); - }); - } + private fileWriteBatch: { resource: URI, content: Uint8Array }[] = []; + private async writeMany() { + return new Promise((c, e) => { + const fileBatch = this.fileWriteBatch; + this.fileWriteBatch = []; + if (fileBatch.length === 0) { return c(); } - hasKey(key: string): Promise { - return new Promise(async (c, e) => { - const transaction = this.database.transaction([this.store]); - const objectStore = transaction.objectStore(this.store); - const request = objectStore.getKey(key); - request.onerror = () => e(request.error); - request.onsuccess = () => { - c(!!request.result); - }; - }); - } - - getValue(key: string): Promise { - return new Promise(async (c, e) => { - const transaction = this.database.transaction([this.store]); - const objectStore = transaction.objectStore(this.store); - const request = objectStore.get(key); - request.onerror = () => e(request.error); - request.onsuccess = () => c(request.result || ''); - }); - } - - setValue(key: string, value: Uint8Array): Promise { - return new Promise(async (c, e) => { const transaction = this.database.transaction([this.store], 'readwrite'); + transaction.onerror = () => e(transaction.error); const objectStore = transaction.objectStore(this.store); - const request = objectStore.put(value, key); - request.onerror = () => e(request.error); + let request: IDBRequest = undefined!; + for (const entry of fileBatch) { + request = objectStore.put(entry.content, entry.resource.path); + } request.onsuccess = () => c(); }); } - deleteKey(key: string): Promise { + private deleteKeys(keys: string[]): Promise { return new Promise(async (c, e) => { + if (keys.length === 0) { return c(); } const transaction = this.database.transaction([this.store], 'readwrite'); + transaction.onerror = () => e(transaction.error); const objectStore = transaction.objectStore(this.store); - const request = objectStore.delete(key); - request.onerror = () => e(request.error); + let request: IDBRequest = undefined!; + for (const key of keys) { + request = objectStore.delete(key); + } + request.onsuccess = () => c(); }); } diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index 23001b2a2..dbc98c13b 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; +import { mark } from 'vs/base/common/performance'; import { Disposable, IDisposable, toDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { isAbsolutePath, dirname, basename, joinPath, IExtUri, extUri, extUriIgnorePathCase } from 'vs/base/common/resources'; -import { localize } from 'vs/nls'; +import { IExtUri, extUri, extUriIgnorePathCase, isAbsolutePath } from 'vs/base/common/resources'; import { TernarySearchTree } from 'vs/base/common/map'; import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays'; -import { getBaseLabel } from 'vs/base/common/labels'; import { ILogService } from 'vs/platform/log/common/log'; -import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, bufferToStream, VSBufferReadableStream, VSBufferReadableBufferedStream, bufferedStreamToBuffer, newWriteableBufferStream } from 'vs/base/common/buffer'; -import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream } from 'vs/base/common/stream'; +import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, VSBufferReadableStream, VSBufferReadableBufferedStream, bufferedStreamToBuffer, newWriteableBufferStream } from 'vs/base/common/buffer'; +import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream, newWriteableStream, IReadableStreamObservable, observe } from 'vs/base/common/stream'; import { Queue } from 'vs/base/common/async'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { Schemas } from 'vs/base/common/network'; @@ -49,6 +49,8 @@ export class FileService extends Disposable implements IFileService { throw new Error(`A filesystem provider for the scheme '${scheme}' is already registered.`); } + mark(`code/registerFilesystem/${scheme}`); + // Add provider with event this.provider.set(scheme, provider); this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider }); @@ -102,7 +104,7 @@ export class FileService extends Disposable implements IFileService { return !!(provider && (provider.capabilities & capability)); } - listCapabilities(): Iterable<{ scheme: string, capabilities: FileSystemProviderCapabilities }> { + listCapabilities(): Iterable<{ scheme: string, capabilities: FileSystemProviderCapabilities; }> { return Iterable.map(this.provider, ([scheme, provider]) => ({ scheme, capabilities: provider.capabilities })); } @@ -215,14 +217,15 @@ export class FileService extends Disposable implements IFileService { }); } - private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise; + private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType; } & Partial, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise; private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat, siblings: number | undefined, resolveMetadata: true, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise; - private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise { + private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType; } & Partial, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise { + const { providerExtUri } = this.getExtUri(provider); // convert to file stat const fileStat: IFileStat = { resource, - name: getBaseLabel(resource), + name: providerExtUri.basename(resource), isFile: (stat.type & FileType.File) !== 0, isDirectory: (stat.type & FileType.Directory) !== 0, isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0, @@ -238,7 +241,7 @@ export class FileService extends Disposable implements IFileService { const entries = await provider.readdir(resource); const resolvedEntries = await Promise.all(entries.map(async ([name, type]) => { try { - const childResource = joinPath(resource, name); + const childResource = providerExtUri.joinPath(resource, name); const childStat = resolveMetadata ? await provider.stat(childResource) : { type }; return await this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse); @@ -263,8 +266,8 @@ export class FileService extends Disposable implements IFileService { return fileStat; } - async resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions }[]): Promise; - async resolveAll(toResolve: { resource: URI, options: IResolveMetadataFileOptions }[]): Promise; + async resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions; }[]): Promise; + async resolveAll(toResolve: { resource: URI, options: IResolveMetadataFileOptions; }[]): Promise; async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise { return Promise.all(toResolve.map(async entry => { try { @@ -327,6 +330,7 @@ export class FileService extends Disposable implements IFileService { async writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise { const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource); + const { providerExtUri } = this.getExtUri(provider); try { @@ -335,7 +339,7 @@ export class FileService extends Disposable implements IFileService { // mkdir recursively as needed if (!stat) { - await this.mkdirp(provider, dirname(resource)); + await this.mkdirp(provider, providerExtUri.dirname(resource)); } // optimization: if the provider has unbuffered write capability and the data @@ -435,7 +439,7 @@ export class FileService extends Disposable implements IFileService { return this.doReadAsFileStream(provider, resource, options); } - private async doReadAsFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & { preferUnbuffered?: boolean }): Promise { + private async doReadAsFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & { preferUnbuffered?: boolean; }): Promise { // install a cancellation token that gets cancelled // when any error occurs. this allows us to resolve @@ -450,6 +454,8 @@ export class FileService extends Disposable implements IFileService { throw error; }); + let fileStreamObserver: IReadableStreamObservable | undefined = undefined; + try { // if the etag is provided, we await the result of the validation @@ -460,30 +466,41 @@ export class FileService extends Disposable implements IFileService { await statPromise; } - let fileStreamPromise: Promise; + let fileStream: VSBufferReadableStream | undefined = undefined; // read unbuffered (only if either preferred, or the provider has no buffered read capability) if (!(hasOpenReadWriteCloseCapability(provider) || hasFileReadStreamCapability(provider)) || (hasReadWriteCapability(provider) && options?.preferUnbuffered)) { - fileStreamPromise = this.readFileUnbuffered(provider, resource, options); + fileStream = this.readFileUnbuffered(provider, resource, options); } // read streamed (always prefer over primitive buffered read) else if (hasFileReadStreamCapability(provider)) { - fileStreamPromise = Promise.resolve(this.readFileStreamed(provider, resource, cancellableSource.token, options)); + fileStream = this.readFileStreamed(provider, resource, cancellableSource.token, options); } // read buffered else { - fileStreamPromise = Promise.resolve(this.readFileBuffered(provider, resource, cancellableSource.token, options)); + fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, options); } - const [fileStat, fileStream] = await Promise.all([statPromise, fileStreamPromise]); + // observe the stream for the error case below + fileStreamObserver = observe(fileStream); + + const fileStat = await statPromise; return { ...fileStat, value: fileStream }; } catch (error) { + + // Await the stream to finish so that we exit this method + // in a consistent state with file handles closed + // (https://github.com/microsoft/vscode/issues/114024) + if (fileStreamObserver) { + await fileStreamObserver.errorOrEnd(); + } + throw new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options); } } @@ -509,23 +526,36 @@ export class FileService extends Disposable implements IFileService { return stream; } - private async readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): Promise { - let buffer = await provider.readFile(resource); + private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): VSBufferReadableStream { + const stream = newWriteableStream(data => VSBuffer.concat(data)); - // respect position option - if (options && typeof options.position === 'number') { - buffer = buffer.slice(options.position); - } + // Read the file into the stream async but do not wait for + // this to complete because streams work via events + (async () => { + try { + let buffer = await provider.readFile(resource); - // respect length option - if (options && typeof options.length === 'number') { - buffer = buffer.slice(0, options.length); - } + // respect position option + if (options && typeof options.position === 'number') { + buffer = buffer.slice(options.position); + } - // Throw if file is too large to load - this.validateReadFileLimits(resource, buffer.byteLength, options); + // respect length option + if (options && typeof options.length === 'number') { + buffer = buffer.slice(0, options.length); + } - return bufferToStream(VSBuffer.wrap(buffer)); + // Throw if file is too large to load + this.validateReadFileLimits(resource, buffer.byteLength, options); + + // End stream with data + stream.end(VSBuffer.wrap(buffer)); + } catch (err) { + stream.error(err); + } + })(); + + return stream; } private async validateReadFile(resource: URI, options?: IReadFileOptions): Promise { @@ -634,7 +664,7 @@ export class FileService extends Disposable implements IFileService { } // create parent folders - await this.mkdirp(targetProvider, dirname(target)); + await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target)); // copy source => target if (mode === 'copy') { @@ -671,7 +701,6 @@ export class FileService extends Disposable implements IFileService { // across providers: copy to target & delete at source else { await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', overwrite); - await this.del(source, { recursive: true }); return 'copy'; @@ -710,7 +739,7 @@ export class FileService extends Disposable implements IFileService { // create children in target if (Array.isArray(sourceFolder.children)) { await Promise.all(sourceFolder.children.map(async sourceChild => { - const targetChild = joinPath(targetFolder, sourceChild.name); + const targetChild = this.getExtUri(targetProvider).providerExtUri.joinPath(targetFolder, sourceChild.name); if (sourceChild.isDirectory) { return this.doCopyFolder(sourceProvider, await this.resolve(sourceChild.resource), targetProvider, targetChild); } else { @@ -720,21 +749,21 @@ export class FileService extends Disposable implements IFileService { } } - private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean, isSameResourceWithDifferentPathCase: boolean }> { + private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean, isSameResourceWithDifferentPathCase: boolean; }> { let isSameResourceWithDifferentPathCase = false; // Check if source is equal or parent to target (requires providers to be the same) if (sourceProvider === targetProvider) { - const { extUri, isPathCaseSensitive } = this.getExtUri(sourceProvider); + const { providerExtUri, isPathCaseSensitive } = this.getExtUri(sourceProvider); if (!isPathCaseSensitive) { - isSameResourceWithDifferentPathCase = extUri.isEqual(source, target); + isSameResourceWithDifferentPathCase = providerExtUri.isEqual(source, target); } if (isSameResourceWithDifferentPathCase && mode === 'copy') { throw new Error(localize('unableToMoveCopyError1', "Unable to copy when source '{0}' is same as target '{1}' with different path case on a case insensitive file system", this.resourceForError(source), this.resourceForError(target))); } - if (!isSameResourceWithDifferentPathCase && extUri.isEqualOrParent(target, source)) { + if (!isSameResourceWithDifferentPathCase && providerExtUri.isEqualOrParent(target, source)) { throw new Error(localize('unableToMoveCopyError2', "Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target))); } } @@ -751,8 +780,8 @@ export class FileService extends Disposable implements IFileService { // Special case: if the target is a parent of the source, we cannot delete // it as it would delete the source as well. In this case we have to throw if (sourceProvider === targetProvider) { - const { extUri } = this.getExtUri(sourceProvider); - if (extUri.isEqualOrParent(source, target)) { + const { providerExtUri } = this.getExtUri(sourceProvider); + if (providerExtUri.isEqualOrParent(source, target)) { throw new Error(localize('unableToMoveCopyError4', "Unable to move/copy '{0}' into '{1}' since a file would replace the folder it is contained in.", this.resourceForError(source), this.resourceForError(target))); } } @@ -761,11 +790,11 @@ export class FileService extends Disposable implements IFileService { return { exists, isSameResourceWithDifferentPathCase }; } - private getExtUri(provider: IFileSystemProvider): { extUri: IExtUri, isPathCaseSensitive: boolean } { + private getExtUri(provider: IFileSystemProvider): { providerExtUri: IExtUri, isPathCaseSensitive: boolean; } { const isPathCaseSensitive = this.isPathCaseSensitive(provider); return { - extUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase, + providerExtUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase, isPathCaseSensitive }; } @@ -791,8 +820,8 @@ export class FileService extends Disposable implements IFileService { const directoriesToCreate: string[] = []; // mkdir until we reach root - const { extUri } = this.getExtUri(provider); - while (!extUri.isEqual(directory, dirname(directory))) { + const { providerExtUri } = this.getExtUri(provider); + while (!providerExtUri.isEqual(directory, providerExtUri.dirname(directory))) { try { const stat = await provider.stat(directory); if ((stat.type & FileType.Directory) === 0) { @@ -808,16 +837,16 @@ export class FileService extends Disposable implements IFileService { } // Upon error, remember directories that need to be created - directoriesToCreate.push(basename(directory)); + directoriesToCreate.push(providerExtUri.basename(directory)); // Continue up - directory = dirname(directory); + directory = providerExtUri.dirname(directory); } } // Create directories as needed for (let i = directoriesToCreate.length - 1; i >= 0; i--) { - directory = joinPath(directory, directoriesToCreate[i]); + directory = providerExtUri.joinPath(directory, directoriesToCreate[i]); try { await provider.mkdir(directory); @@ -894,11 +923,11 @@ export class FileService extends Disposable implements IFileService { private readonly _onDidFilesChange = this._register(new Emitter()); readonly onDidFilesChange = this._onDidFilesChange.event; - private readonly activeWatchers = new Map(); + private readonly activeWatchers = new Map(); watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IDisposable { let watchDisposed = false; - let watchDisposable = toDisposable(() => watchDisposed = true); + let disposeWatch = () => { watchDisposed = true; }; // Watch and wire in disposable which is async but // check if we got disposed meanwhile and forward @@ -906,11 +935,11 @@ export class FileService extends Disposable implements IFileService { if (watchDisposed) { dispose(disposable); } else { - watchDisposable = disposable; + disposeWatch = () => dispose(disposable); } }, error => this.logService.error(error)); - return toDisposable(() => dispose(watchDisposable)); + return toDisposable(() => disposeWatch()); } async doWatch(resource: URI, options: IWatchOptions): Promise { @@ -940,12 +969,12 @@ export class FileService extends Disposable implements IFileService { } private toWatchKey(provider: IFileSystemProvider, resource: URI, options: IWatchOptions): string { - const { extUri } = this.getExtUri(provider); + const { providerExtUri } = this.getExtUri(provider); return [ - extUri.getComparisonKey(resource), // lowercase path if the provider is case insensitive - String(options.recursive), // use recursive: true | false as part of the key - options.excludes.join() // use excludes as part of the key + providerExtUri.getComparisonKey(resource), // lowercase path if the provider is case insensitive + String(options.recursive), // use recursive: true | false as part of the key + options.excludes.join() // use excludes as part of the key ].join(); } @@ -963,8 +992,8 @@ export class FileService extends Disposable implements IFileService { private readonly writeQueues: Map> = new Map(); private ensureWriteQueue(provider: IFileSystemProvider, resource: URI): Queue { - const { extUri } = this.getExtUri(provider); - const queueKey = extUri.getComparisonKey(resource); + const { providerExtUri } = this.getExtUri(provider); + const queueKey = providerExtUri.getComparisonKey(resource); // ensure to never write to the same resource without finishing // the one write. this ensures a write finishes consistently diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 4ae154345..9b91c3dc1 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -278,7 +278,7 @@ export interface IFileSystemProvider { readonly capabilities: FileSystemProviderCapabilities; readonly onDidChangeCapabilities: Event; - readonly onDidErrorOccur?: Event; // TODO@ben remove once file watchers are solid + readonly onDidErrorOccur?: Event; // TODO@bpasero remove once file watchers are solid readonly onDidChangeFile: Event; watch(resource: URI, opts: IWatchOptions): IDisposable; @@ -947,9 +947,9 @@ export function etag(stat: { mtime: number | undefined, size: number | undefined return stat.mtime.toString(29) + stat.size.toString(31); } -export function whenProviderRegistered(file: URI, fileService: IFileService): Promise { +export async function whenProviderRegistered(file: URI, fileService: IFileService): Promise { if (fileService.canHandleResource(URI.from({ scheme: file.scheme }))) { - return Promise.resolve(); + return; } return new Promise(resolve => { diff --git a/src/vs/platform/files/common/io.ts b/src/vs/platform/files/common/io.ts index 141241159..592b51168 100644 --- a/src/vs/platform/files/common/io.ts +++ b/src/vs/platform/files/common/io.ts @@ -58,10 +58,11 @@ async function doReadFileIntoStream(provider: IFileSystemProviderWithOpenRead // open handle through provider const handle = await provider.open(resource, { create: false }); - // Check for cancellation - throwIfCancelled(token); - try { + + // Check for cancellation + throwIfCancelled(token); + let totalBytesRead = 0; let bytesRead = 0; let allowedRemainingBytes = (options && typeof options.length === 'number') ? options.length : undefined; diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 89408a3f9..75811f569 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -522,7 +522,7 @@ export class DiskFileSystemProvider extends Disposable implements return this.watchRecursive(resource, opts.excludes); } - return this.watchNonRecursive(resource); // TODO@ben ideally the same watcher can be used in both cases + return this.watchNonRecursive(resource); // TODO@bpasero ideally the same watcher can be used in both cases } private watchRecursive(resource: URI, excludes: string[]): IDisposable { diff --git a/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts b/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts index 51a4b1a65..70c4cfd61 100644 --- a/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts +++ b/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as glob from 'vs/base/common/glob'; -import * as extpath from 'vs/base/common/extpath'; -import * as path from 'vs/base/common/path'; -import * as platform from 'vs/base/common/platform'; -import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import * as nsfw from 'vscode-nsfw'; +import * as glob from 'vs/base/common/glob'; +import { join } from 'vs/base/common/path'; +import { isMacintosh } from 'vs/base/common/platform'; +import { isEqualOrParent } from 'vs/base/common/extpath'; +import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import { IWatcherService, IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher'; import { ThrottledDelayer } from 'vs/base/common/async'; import { FileChangeType } from 'vs/platform/files/common/files'; @@ -111,7 +111,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { // We have to detect this case and massage the events to correct this. let realBasePathDiffers = false; let realBasePathLength = request.path.length; - if (platform.isMacintosh) { + if (isMacintosh) { try { // First check for symbolic link @@ -141,7 +141,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { for (const e of events) { // Logging if (this.verboseLogging) { - const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : path.join(e.directory, e.file || ''); + const logPath = e.action === nsfw.actions.RENAMED ? join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : join(e.directory, e.file || ''); this.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`); } @@ -149,20 +149,20 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { let absolutePath: string; if (e.action === nsfw.actions.RENAMED) { // Rename fires when a file's name changes within a single directory - absolutePath = path.join(e.directory, e.oldFile || ''); + absolutePath = join(e.directory, e.oldFile || ''); if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath }); } else if (this.verboseLogging) { this.log(` >> ignored ${absolutePath}`); } - absolutePath = path.join(e.newDirectory || e.directory, e.newFile || ''); + absolutePath = join(e.newDirectory || e.directory, e.newFile || ''); if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath }); } else if (this.verboseLogging) { this.log(` >> ignored ${absolutePath}`); } } else { - absolutePath = path.join(e.directory, e.file || ''); + absolutePath = join(e.directory, e.file || ''); if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: nsfwActionToRawChangeType[e.action], @@ -179,7 +179,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { const events = undeliveredFileEvents; undeliveredFileEvents = []; - if (platform.isMacintosh) { + if (isMacintosh) { events.forEach(e => { // Mac uses NFD unicode form on disk, but we want NFC @@ -230,7 +230,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { // Normalizes a set of root paths by removing any root paths that are // sub-paths of other roots. return roots.filter(r => roots.every(other => { - return !(r.path.length > other.path.length && extpath.isEqualOrParent(r.path, other.path)); + return !(r.path.length > other.path.length && isEqualOrParent(r.path, other.path)); })); } diff --git a/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts b/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts index 9ad083fc9..0469c3f63 100644 --- a/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts +++ b/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts @@ -30,28 +30,28 @@ suite('NSFW Watcher Service', async () => { test('should not impacts roots that don\'t overlap', () => { const service = new TestNsfwWatcherService(); if (platform.isWindows) { - assert.deepEqual(service.normalizeRoots(['C:\\a']), ['C:\\a']); - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a']), ['C:\\a']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); } else { - assert.deepEqual(service.normalizeRoots(['/a']), ['/a']); - assert.deepEqual(service.normalizeRoots(['/a', '/b']), ['/a', '/b']); - assert.deepEqual(service.normalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); + assert.deepStrictEqual(service.normalizeRoots(['/a']), ['/a']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/b']), ['/a', '/b']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); } }); test('should remove sub-folders of other roots', () => { const service = new TestNsfwWatcherService(); if (platform.isWindows) { - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']); - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); - assert.deepEqual(service.normalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); } else { - assert.deepEqual(service.normalizeRoots(['/a', '/a/b']), ['/a']); - assert.deepEqual(service.normalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']); - assert.deepEqual(service.normalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); - assert.deepEqual(service.normalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/a/b']), ['/a']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']); + assert.deepStrictEqual(service.normalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']); } }); }); diff --git a/src/vs/platform/files/node/watcher/nsfw/watcherService.ts b/src/vs/platform/files/node/watcher/nsfw/watcherService.ts index 79fa3ba25..3b0cd433f 100644 --- a/src/vs/platform/files/node/watcher/nsfw/watcherService.ts +++ b/src/vs/platform/files/node/watcher/nsfw/watcherService.ts @@ -39,9 +39,9 @@ export class FileWatcher extends Disposable { serverName: 'File Watcher (nsfw)', args: ['--type=watcherService'], env: { - AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/nsfw/watcherApp', - PIPE_LOGGING: 'true', - VERBOSE_LOGGING: 'true' // transmit console logs from server to client + VSCODE_AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/nsfw/watcherApp', + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client } } )); diff --git a/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts b/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts index 895e5dfa9..25276aa2a 100644 --- a/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts +++ b/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts @@ -6,9 +6,8 @@ import * as chokidar from 'chokidar'; import * as fs from 'fs'; import * as gracefulFs from 'graceful-fs'; -gracefulFs.gracefulify(fs); -import * as extpath from 'vs/base/common/extpath'; import * as glob from 'vs/base/common/glob'; +import { isEqualOrParent } from 'vs/base/common/extpath'; import { FileChangeType } from 'vs/platform/files/common/files'; import { ThrottledDelayer } from 'vs/base/common/async'; import { normalizeNFC } from 'vs/base/common/normalization'; @@ -20,6 +19,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { equals } from 'vs/base/common/arrays'; import { Disposable } from 'vs/base/common/lifecycle'; +gracefulFs.gracefulify(fs); // enable gracefulFs + process.noAsar = true; // disable ASAR support in watcher process interface IWatcher { @@ -311,7 +312,7 @@ function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean { return false; } - if (extpath.isEqualOrParent(path, request.path)) { + if (isEqualOrParent(path, request.path)) { if (!request.parsedPattern) { if (request.excludes && request.excludes.length > 0) { const pattern = `{${request.excludes.join(',')}}`; @@ -343,7 +344,7 @@ export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string for (const request of requests) { const basePath = request.path; const ignored = (request.excludes || []).sort(); - if (prevRequest && (extpath.isEqualOrParent(basePath, prevRequest.path))) { + if (prevRequest && (isEqualOrParent(basePath, prevRequest.path))) { if (!isEqualIgnore(ignored, prevRequest.excludes)) { result[prevRequest.path].push({ path: basePath, excludes: ignored }); } diff --git a/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts b/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts index 4c780ec6b..8b475de9a 100644 --- a/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts +++ b/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts @@ -20,18 +20,18 @@ suite('Chokidar normalizeRoots', async () => { function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) { const requests = inputPaths.map(path => newRequest(path)); const actual = normalizeRoots(requests); - assert.deepEqual(Object.keys(actual).sort(), expectedPaths); + assert.deepStrictEqual(Object.keys(actual).sort(), expectedPaths); } function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) { const actual = normalizeRoots(inputRequests); const actualPath = Object.keys(actual).sort(); const expectedPaths = Object.keys(expectedRequests).sort(); - assert.deepEqual(actualPath, expectedPaths); + assert.deepStrictEqual(actualPath, expectedPaths); for (let path of actualPath) { let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path)); let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path)); - assert.deepEqual(a, e); + assert.deepStrictEqual(a, e); } } diff --git a/src/vs/platform/files/node/watcher/unix/watcherService.ts b/src/vs/platform/files/node/watcher/unix/watcherService.ts index e561cc8e5..75f1bcf90 100644 --- a/src/vs/platform/files/node/watcher/unix/watcherService.ts +++ b/src/vs/platform/files/node/watcher/unix/watcherService.ts @@ -40,9 +40,9 @@ export class FileWatcher extends Disposable { serverName: 'File Watcher (chokidar)', args: ['--type=watcherService'], env: { - AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/unix/watcherApp', - PIPE_LOGGING: 'true', - VERBOSE_LOGGING: 'true' // transmit console logs from server to client + VSCODE_AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/unix/watcherApp', + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client } } )); diff --git a/src/vs/platform/files/test/browser/fileService.test.ts b/src/vs/platform/files/test/browser/fileService.test.ts index edc32f0a3..e8d53c1cf 100644 --- a/src/vs/platform/files/test/browser/fileService.test.ts +++ b/src/vs/platform/files/test/browser/fileService.test.ts @@ -19,7 +19,7 @@ suite('File Service', () => { const resource = URI.parse('test://foo/bar'); const provider = new NullFileSystemProvider(); - assert.equal(service.canHandleResource(resource), false); + assert.strictEqual(service.canHandleResource(resource), false); const registrations: IFileSystemProviderRegistrationEvent[] = []; service.onDidChangeFileSystemProviderRegistrations(e => { @@ -47,33 +47,33 @@ suite('File Service', () => { await service.activateProvider('test'); - assert.equal(service.canHandleResource(resource), true); + assert.strictEqual(service.canHandleResource(resource), true); - assert.equal(registrations.length, 1); - assert.equal(registrations[0].scheme, 'test'); - assert.equal(registrations[0].added, true); + assert.strictEqual(registrations.length, 1); + assert.strictEqual(registrations[0].scheme, 'test'); + assert.strictEqual(registrations[0].added, true); assert.ok(registrationDisposable); - assert.equal(capabilityChanges.length, 0); + assert.strictEqual(capabilityChanges.length, 0); provider.setCapabilities(FileSystemProviderCapabilities.FileFolderCopy); - assert.equal(capabilityChanges.length, 1); + assert.strictEqual(capabilityChanges.length, 1); provider.setCapabilities(FileSystemProviderCapabilities.Readonly); - assert.equal(capabilityChanges.length, 2); + assert.strictEqual(capabilityChanges.length, 2); await service.activateProvider('test'); - assert.equal(callCount, 2); // activation is called again + assert.strictEqual(callCount, 2); // activation is called again - assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true); - assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false); + assert.strictEqual(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true); + assert.strictEqual(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false); registrationDisposable!.dispose(); - assert.equal(service.canHandleResource(resource), false); + assert.strictEqual(service.canHandleResource(resource), false); - assert.equal(registrations.length, 2); - assert.equal(registrations[1].scheme, 'test'); - assert.equal(registrations[1].added, false); + assert.strictEqual(registrations.length, 2); + assert.strictEqual(registrations[1].scheme, 'test'); + assert.strictEqual(registrations[1].added, false); }); test('watch', async () => { @@ -91,9 +91,9 @@ suite('File Service', () => { const watcher1Disposable = service.watch(resource1); await timeout(0); // service.watch() is async - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher1Disposable.dispose(); - assert.equal(disposeCounter, 1); + assert.strictEqual(disposeCounter, 1); disposeCounter = 0; const resource2 = URI.parse('test://foo/bar2'); @@ -102,13 +102,13 @@ suite('File Service', () => { const watcher2Disposable3 = service.watch(resource2); await timeout(0); // service.watch() is async - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher2Disposable1.dispose(); - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher2Disposable2.dispose(); - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher2Disposable3.dispose(); - assert.equal(disposeCounter, 1); + assert.strictEqual(disposeCounter, 1); disposeCounter = 0; const resource3 = URI.parse('test://foo/bar3'); @@ -116,10 +116,10 @@ suite('File Service', () => { const watcher3Disposable2 = service.watch(resource3, { recursive: true, excludes: [] }); await timeout(0); // service.watch() is async - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher3Disposable1.dispose(); - assert.equal(disposeCounter, 1); + assert.strictEqual(disposeCounter, 1); watcher3Disposable2.dispose(); - assert.equal(disposeCounter, 2); + assert.strictEqual(disposeCounter, 2); }); }); diff --git a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts index abb3034a3..67ccfd9a6 100644 --- a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts +++ b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts @@ -6,20 +6,20 @@ import * as assert from 'assert'; import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; -import { posix } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; -import { FileOperation, FileOperationEvent } from 'vs/platform/files/common/files'; +import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileSystemProviderErrorCode, FileType, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { NullLogService } from 'vs/platform/log/common/log'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IIndexedDBFileSystemProvider, IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, INDEXEDDB_USERDATA_OBJECT_STORE } from 'vs/platform/files/browser/indexedDBFileSystemProvider'; import { assertIsDefined } from 'vs/base/common/types'; - -// FileService doesn't work with \ leading a path. Windows join swaps /'s for \'s, -// making /-style absolute paths fail isAbsolute checks. -const join = posix.join; +import { basename, joinPath } from 'vs/base/common/resources'; +import { bufferToReadable, bufferToStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; suite('IndexedDB File Service', function () { + // IDB sometimes under pressure in build machines. + this.retries(3); + const logSchema = 'logs'; let service: FileService; @@ -27,12 +27,43 @@ suite('IndexedDB File Service', function () { let userdataFileProvider: IIndexedDBFileSystemProvider; const testDir = '/'; - const makeLogfileURI = (path: string) => URI.from({ scheme: logSchema, path }); - const makeUserdataURI = (path: string) => URI.from({ scheme: Schemas.userData, path }); + const logfileURIFromPaths = (paths: string[]) => joinPath(URI.from({ scheme: logSchema, path: testDir }), ...paths); + const userdataURIFromPaths = (paths: readonly string[]) => joinPath(URI.from({ scheme: Schemas.userData, path: testDir }), ...paths); const disposables = new DisposableStore(); - setup(async () => { + const initFixtures = async () => { + await Promise.all( + [['fixtures', 'resolver', 'examples'], + ['fixtures', 'resolver', 'other', 'deep'], + ['fixtures', 'service', 'deep'], + ['batched']] + .map(path => userdataURIFromPaths(path)) + .map(uri => service.createFolder(uri))); + await Promise.all( + ([ + [['fixtures', 'resolver', 'examples', 'company.js'], 'class company {}'], + [['fixtures', 'resolver', 'examples', 'conway.js'], 'export function conway() {}'], + [['fixtures', 'resolver', 'examples', 'employee.js'], 'export const employee = "jax"'], + [['fixtures', 'resolver', 'examples', 'small.js'], ''], + [['fixtures', 'resolver', 'other', 'deep', 'company.js'], 'class company {}'], + [['fixtures', 'resolver', 'other', 'deep', 'conway.js'], 'export function conway() {}'], + [['fixtures', 'resolver', 'other', 'deep', 'employee.js'], 'export const employee = "jax"'], + [['fixtures', 'resolver', 'other', 'deep', 'small.js'], ''], + [['fixtures', 'resolver', 'index.html'], '

    p

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

    p

    '], + ] as const) + .map(([path, contents]) => [userdataURIFromPaths(path), contents] as const) + .map(([uri, contents]) => service.createFile(uri, VSBuffer.fromString(contents))) + ); + }; + + const reload = async () => { const logService = new NullLogService(); service = new FileService(logService); @@ -45,33 +76,302 @@ suite('IndexedDB File Service', function () { userdataFileProvider = assertIsDefined(await new IndexedDB().createFileSystemProvider(logSchema, INDEXEDDB_USERDATA_OBJECT_STORE)); disposables.add(service.registerProvider(Schemas.userData, userdataFileProvider)); disposables.add(userdataFileProvider); + }; + + setup(async () => { + await reload(); }); teardown(async () => { disposables.clear(); + await logFileProvider.delete(logfileURIFromPaths([]), { recursive: true, useTrash: false }); + await userdataFileProvider.delete(userdataURIFromPaths([]), { recursive: true, useTrash: false }); + }); - await logFileProvider.delete(makeLogfileURI(testDir), { recursive: true, useTrash: false }); - await userdataFileProvider.delete(makeUserdataURI(testDir), { recursive: true, useTrash: false }); + test('root is always present', async () => { + assert.strictEqual((await userdataFileProvider.stat(userdataURIFromPaths([]))).type, FileType.Directory); + await userdataFileProvider.delete(userdataURIFromPaths([]), { recursive: true, useTrash: false }); + assert.strictEqual((await userdataFileProvider.stat(userdataURIFromPaths([]))).type, FileType.Directory); }); test('createFolder', async () => { let event: FileOperationEvent | undefined; disposables.add(service.onDidRunOperation(e => event = e)); - const parent = await service.resolve(makeUserdataURI(testDir)); + const parent = await service.resolve(userdataURIFromPaths([])); + const newFolderResource = joinPath(parent.resource, 'newFolder'); - const newFolderResource = makeUserdataURI(join(parent.resource.path, 'newFolder')); - - assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 0); + assert.strictEqual((await userdataFileProvider.readdir(parent.resource)).length, 0); const newFolder = await service.createFolder(newFolderResource); - assert.equal(newFolder.name, 'newFolder'); - // Invalid.. dirs dont exist in our IDBFSB. - // assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 1); + assert.strictEqual(newFolder.name, 'newFolder'); + assert.strictEqual((await userdataFileProvider.readdir(parent.resource)).length, 1); + assert.strictEqual((await userdataFileProvider.stat(newFolderResource)).type, FileType.Directory); assert.ok(event); - assert.equal(event!.resource.path, newFolderResource.path); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.path, newFolderResource.path); - assert.equal(event!.target!.isDirectory, true); + assert.strictEqual(event!.resource.path, newFolderResource.path); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.path, newFolderResource.path); + assert.strictEqual(event!.target!.isDirectory, true); + }); + + test('createFolder: creating multiple folders at once', async () => { + let event: FileOperationEvent; + disposables.add(service.onDidRunOperation(e => event = e)); + + const multiFolderPaths = ['a', 'couple', 'of', 'folders']; + const parent = await service.resolve(userdataURIFromPaths([])); + const newFolderResource = joinPath(parent.resource, ...multiFolderPaths); + + const newFolder = await service.createFolder(newFolderResource); + + const lastFolderName = multiFolderPaths[multiFolderPaths.length - 1]; + assert.strictEqual(newFolder.name, lastFolderName); + assert.strictEqual((await userdataFileProvider.stat(newFolderResource)).type, FileType.Directory); + + assert.ok(event!); + assert.strictEqual(event!.resource.path, newFolderResource.path); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.path, newFolderResource.path); + assert.strictEqual(event!.target!.isDirectory, true); + }); + + test('exists', async () => { + let exists = await service.exists(userdataURIFromPaths([])); + assert.strictEqual(exists, true); + + exists = await service.exists(userdataURIFromPaths(['hello'])); + assert.strictEqual(exists, false); + }); + + test('resolve - file', async () => { + await initFixtures(); + + const resource = userdataURIFromPaths(['fixtures', 'resolver', 'index.html']); + const resolved = await service.resolve(resource); + + assert.strictEqual(resolved.name, 'index.html'); + assert.strictEqual(resolved.isFile, true); + assert.strictEqual(resolved.isDirectory, false); + assert.strictEqual(resolved.isSymbolicLink, false); + assert.strictEqual(resolved.resource.toString(), resource.toString()); + assert.strictEqual(resolved.children, undefined); + assert.ok(resolved.size! > 0); + }); + + test('resolve - directory', async () => { + await initFixtures(); + + const testsElements = ['examples', 'other', 'index.html', 'site.css']; + + const resource = userdataURIFromPaths(['fixtures', 'resolver']); + const result = await service.resolve(resource); + + assert.ok(result); + assert.strictEqual(result.resource.toString(), resource.toString()); + assert.strictEqual(result.name, 'resolver'); + assert.ok(result.children); + assert.ok(result.children!.length > 0); + assert.ok(result!.isDirectory); + assert.strictEqual(result.children!.length, testsElements.length); + + assert.ok(result.children!.every(entry => { + return testsElements.some(name => { + return basename(entry.resource) === name; + }); + })); + + result.children!.forEach(value => { + assert.ok(basename(value.resource)); + if (['examples', 'other'].indexOf(basename(value.resource)) >= 0) { + assert.ok(value.isDirectory); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); + } else if (basename(value.resource) === 'index.html') { + assert.ok(!value.isDirectory); + assert.ok(!value.children); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); + } else if (basename(value.resource) === 'site.css') { + assert.ok(!value.isDirectory); + assert.ok(!value.children); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); + } else { + assert.ok(!'Unexpected value ' + basename(value.resource)); + } + }); + }); + + test('createFile', async () => { + return assertCreateFile(contents => VSBuffer.fromString(contents)); + }); + + test('createFile (readable)', async () => { + return assertCreateFile(contents => bufferToReadable(VSBuffer.fromString(contents))); + }); + + test('createFile (stream)', async () => { + return assertCreateFile(contents => bufferToStream(VSBuffer.fromString(contents))); + }); + + async function assertCreateFile(converter: (content: string) => VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise { + let event: FileOperationEvent; + disposables.add(service.onDidRunOperation(e => event = e)); + + const contents = 'Hello World'; + const resource = userdataURIFromPaths(['test.txt']); + + assert.strictEqual(await service.canCreateFile(resource), true); + const fileStat = await service.createFile(resource, converter(contents)); + assert.strictEqual(fileStat.name, 'test.txt'); + assert.strictEqual((await userdataFileProvider.stat(fileStat.resource)).type, FileType.File); + assert.strictEqual(new TextDecoder().decode(await userdataFileProvider.readFile(fileStat.resource)), contents); + + assert.ok(event!); + assert.strictEqual(event!.resource.path, resource.path); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.path, resource.path); + } + + const makeBatchTester = (size: number, name: string) => { + const batch = Array.from({ length: 50 }).map((_, i) => ({ contents: `Hello${i}`, resource: userdataURIFromPaths(['batched', name, `Hello${i}.txt`]) })); + let stats: Promise | undefined = undefined; + return { + async create() { + return stats = Promise.all(batch.map(entry => service.createFile(entry.resource, VSBuffer.fromString(entry.contents)))); + }, + async assertContentsCorrect() { + await Promise.all(batch.map(async (entry, i) => { + if (!stats) { throw Error('read called before create'); } + const stat = (await stats!)[i]; + assert.strictEqual(stat.name, `Hello${i}.txt`); + assert.strictEqual((await userdataFileProvider.stat(stat.resource)).type, FileType.File); + assert.strictEqual(new TextDecoder().decode(await userdataFileProvider.readFile(stat.resource)), entry.contents); + })); + }, + async delete() { + await service.del(userdataURIFromPaths(['batched', name]), { recursive: true, useTrash: false }); + }, + async assertContentsEmpty() { + if (!stats) { throw Error('assertContentsEmpty called before create'); } + await Promise.all((await stats).map(async stat => { + const newStat = await userdataFileProvider.stat(stat.resource).catch(e => e.code); + assert.strictEqual(newStat, FileSystemProviderErrorCode.FileNotFound); + })); + } + }; + }; + + test('createFile (small batch)', async () => { + const tester = makeBatchTester(50, 'smallBatch'); + await tester.create(); + await tester.assertContentsCorrect(); + await tester.delete(); + await tester.assertContentsEmpty(); + }); + + test('createFile (mixed parallel/sequential)', async () => { + const single1 = makeBatchTester(1, 'single1'); + const single2 = makeBatchTester(1, 'single2'); + + const batch1 = makeBatchTester(20, 'batch1'); + const batch2 = makeBatchTester(20, 'batch2'); + + single1.create(); + batch1.create(); + await Promise.all([single1.assertContentsCorrect(), batch1.assertContentsCorrect()]); + single2.create(); + batch2.create(); + await Promise.all([single2.assertContentsCorrect(), batch2.assertContentsCorrect()]); + await Promise.all([single1.assertContentsCorrect(), batch1.assertContentsCorrect()]); + + await (Promise.all([single1.delete(), single2.delete(), batch1.delete(), batch2.delete()])); + await (Promise.all([single1.assertContentsEmpty(), single2.assertContentsEmpty(), batch1.assertContentsEmpty(), batch2.assertContentsEmpty()])); + }); + + test('deleteFile', async () => { + await initFixtures(); + + let event: FileOperationEvent; + disposables.add(service.onDidRunOperation(e => event = e)); + + const anotherResource = userdataURIFromPaths(['fixtures', 'service', 'deep', 'company.js']); + const resource = userdataURIFromPaths(['fixtures', 'service', 'deep', 'conway.js']); + const source = await service.resolve(resource); + + assert.strictEqual(await service.canDelete(source.resource, { useTrash: false }), true); + await service.del(source.resource, { useTrash: false }); + + assert.strictEqual(await service.exists(source.resource), false); + assert.strictEqual(await service.exists(anotherResource), true); + + assert.ok(event!); + assert.strictEqual(event!.resource.path, resource.path); + assert.strictEqual(event!.operation, FileOperation.DELETE); + + { + let error: Error | undefined = undefined; + try { + await service.del(source.resource, { useTrash: false }); + } catch (e) { + error = e; + } + + assert.ok(error); + assert.strictEqual((error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND); + } + await reload(); + { + let error: Error | undefined = undefined; + try { + await service.del(source.resource, { useTrash: false }); + } catch (e) { + error = e; + } + + assert.ok(error); + assert.strictEqual((error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND); + } + }); + + test('deleteFolder (recursive)', async () => { + await initFixtures(); + let event: FileOperationEvent; + disposables.add(service.onDidRunOperation(e => event = e)); + + const resource = userdataURIFromPaths(['fixtures', 'service', 'deep']); + const subResource1 = userdataURIFromPaths(['fixtures', 'service', 'deep', 'company.js']); + const subResource2 = userdataURIFromPaths(['fixtures', 'service', 'deep', 'conway.js']); + assert.strictEqual(await service.exists(subResource1), true); + assert.strictEqual(await service.exists(subResource2), true); + + const source = await service.resolve(resource); + + assert.strictEqual(await service.canDelete(source.resource, { recursive: true, useTrash: false }), true); + await service.del(source.resource, { recursive: true, useTrash: false }); + + assert.strictEqual(await service.exists(source.resource), false); + assert.strictEqual(await service.exists(subResource1), false); + assert.strictEqual(await service.exists(subResource2), false); + assert.ok(event!); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); + }); + + + test('deleteFolder (non recursive)', async () => { + await initFixtures(); + const resource = userdataURIFromPaths(['fixtures', 'service', 'deep']); + const source = await service.resolve(resource); + + assert.ok((await service.canDelete(source.resource)) instanceof Error); + + let error; + try { + await service.del(source.resource); + } catch (e) { + error = e; + } + assert.ok(error); }); }); diff --git a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts index 4f7a0b94c..4c8f13fc8 100644 --- a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts +++ b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts @@ -8,11 +8,10 @@ import { tmpdir } from 'os'; import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { generateUuid } from 'vs/base/common/uuid'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { join, basename, dirname, posix } from 'vs/base/common/path'; import { getPathFromAmdModule } from 'vs/base/common/amd'; -import { copy, rimraf, symlink, RimRafMode, rimrafSync } from 'vs/base/node/pfs'; +import { copy, rimraf, symlink, rimrafSync } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream } from 'fs'; import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata } from 'vs/platform/files/common/files'; @@ -118,26 +117,18 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider { } } -suite('Disk File Service', function () { +flakySuite('Disk File Service', function () { - const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'diskfileservice'); const testSchema = 'test'; let service: FileService; let fileProvider: TestDiskFileSystemProvider; let testProvider: TestDiskFileSystemProvider; + let testDir: string; const disposables = new DisposableStore(); - // Given issues such as https://github.com/microsoft/vscode/issues/78602 - // and https://github.com/microsoft/vscode/issues/92334 we see random test - // failures when accessing the native file system. To diagnose further, we - // retry node.js file access tests up to 3 times to rule out any random disk - // issue and increase the timeout. - this.retries(3); - this.timeout(1000 * 10); - setup(async () => { const logService = new NullLogService(); @@ -152,17 +143,17 @@ suite('Disk File Service', function () { disposables.add(service.registerProvider(testSchema, testProvider)); disposables.add(testProvider); - const id = generateUuid(); - testDir = join(parentDir, id); + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'diskfileservice'); + const sourceDir = getPathFromAmdModule(require, './fixtures/service'); await copy(sourceDir, testDir); }); - teardown(async () => { + teardown(() => { disposables.clear(); - await rimraf(parentDir, RimRafMode.MOVE); + return rimraf(testDir); }); test('createFolder', async () => { @@ -175,14 +166,14 @@ suite('Disk File Service', function () { const newFolder = await service.createFolder(newFolderResource); - assert.equal(newFolder.name, 'newFolder'); - assert.equal(existsSync(newFolder.resource.fsPath), true); + assert.strictEqual(newFolder.name, 'newFolder'); + assert.strictEqual(existsSync(newFolder.resource.fsPath), true); assert.ok(event); - assert.equal(event!.resource.fsPath, newFolderResource.fsPath); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.fsPath, newFolderResource.fsPath); - assert.equal(event!.target!.isDirectory, true); + assert.strictEqual(event!.resource.fsPath, newFolderResource.fsPath); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.fsPath, newFolderResource.fsPath); + assert.strictEqual(event!.target!.isDirectory, true); }); test('createFolder: creating multiple folders at once', async () => { @@ -197,34 +188,34 @@ suite('Disk File Service', function () { const newFolder = await service.createFolder(newFolderResource); const lastFolderName = multiFolderPaths[multiFolderPaths.length - 1]; - assert.equal(newFolder.name, lastFolderName); - assert.equal(existsSync(newFolder.resource.fsPath), true); + assert.strictEqual(newFolder.name, lastFolderName); + assert.strictEqual(existsSync(newFolder.resource.fsPath), true); assert.ok(event!); - assert.equal(event!.resource.fsPath, newFolderResource.fsPath); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.fsPath, newFolderResource.fsPath); - assert.equal(event!.target!.isDirectory, true); + assert.strictEqual(event!.resource.fsPath, newFolderResource.fsPath); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.fsPath, newFolderResource.fsPath); + assert.strictEqual(event!.target!.isDirectory, true); }); test('exists', async () => { let exists = await service.exists(URI.file(testDir)); - assert.equal(exists, true); + assert.strictEqual(exists, true); exists = await service.exists(URI.file(testDir + 'something')); - assert.equal(exists, false); + assert.strictEqual(exists, false); }); test('resolve - file', async () => { const resource = URI.file(getPathFromAmdModule(require, './fixtures/resolver/index.html')); const resolved = await service.resolve(resource); - assert.equal(resolved.name, 'index.html'); - assert.equal(resolved.isFile, true); - assert.equal(resolved.isDirectory, false); - assert.equal(resolved.isSymbolicLink, false); - assert.equal(resolved.resource.toString(), resource.toString()); - assert.equal(resolved.children, undefined); + assert.strictEqual(resolved.name, 'index.html'); + assert.strictEqual(resolved.isFile, true); + assert.strictEqual(resolved.isDirectory, false); + assert.strictEqual(resolved.isSymbolicLink, false); + assert.strictEqual(resolved.resource.toString(), resource.toString()); + assert.strictEqual(resolved.children, undefined); assert.ok(resolved.mtime! > 0); assert.ok(resolved.ctime! > 0); assert.ok(resolved.size! > 0); @@ -237,14 +228,14 @@ suite('Disk File Service', function () { const result = await service.resolve(resource); assert.ok(result); - assert.equal(result.resource.toString(), resource.toString()); - assert.equal(result.name, 'resolver'); + assert.strictEqual(result.resource.toString(), resource.toString()); + assert.strictEqual(result.name, 'resolver'); assert.ok(result.children); assert.ok(result.children!.length > 0); assert.ok(result!.isDirectory); assert.ok(result.mtime! > 0); assert.ok(result.ctime! > 0); - assert.equal(result.children!.length, testsElements.length); + assert.strictEqual(result.children!.length, testsElements.length); assert.ok(result.children!.every(entry => { return testsElements.some(name => { @@ -256,18 +247,18 @@ suite('Disk File Service', function () { assert.ok(basename(value.resource.fsPath)); if (['examples', 'other'].indexOf(basename(value.resource.fsPath)) >= 0) { assert.ok(value.isDirectory); - assert.equal(value.mtime, undefined); - assert.equal(value.ctime, undefined); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); } else if (basename(value.resource.fsPath) === 'index.html') { assert.ok(!value.isDirectory); assert.ok(!value.children); - assert.equal(value.mtime, undefined); - assert.equal(value.ctime, undefined); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); } else if (basename(value.resource.fsPath) === 'site.css') { assert.ok(!value.isDirectory); assert.ok(!value.children); - assert.equal(value.mtime, undefined); - assert.equal(value.ctime, undefined); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); } else { assert.ok(!'Unexpected value ' + basename(value.resource.fsPath)); } @@ -280,13 +271,13 @@ suite('Disk File Service', function () { const result = await service.resolve(URI.file(getPathFromAmdModule(require, './fixtures/resolver')), { resolveMetadata: true }); assert.ok(result); - assert.equal(result.name, 'resolver'); + assert.strictEqual(result.name, 'resolver'); assert.ok(result.children); assert.ok(result.children!.length > 0); assert.ok(result!.isDirectory); assert.ok(result.mtime! > 0); assert.ok(result.ctime! > 0); - assert.equal(result.children!.length, testsElements.length); + assert.strictEqual(result.children!.length, testsElements.length); assert.ok(result.children!.every(entry => { return testsElements.some(name => { @@ -320,10 +311,10 @@ suite('Disk File Service', function () { test('resolve - directory with resolveTo', async () => { const resolved = await service.resolve(URI.file(testDir), { resolveTo: [URI.file(join(testDir, 'deep'))] }); - assert.equal(resolved.children!.length, 8); + assert.strictEqual(resolved.children!.length, 8); const deep = (getByName(resolved, 'deep')!); - assert.equal(deep.children!.length, 4); + assert.strictEqual(deep.children!.length, 4); }); test('resolve - directory - resolveTo single directory', async () => { @@ -336,7 +327,7 @@ suite('Disk File Service', function () { assert.ok(result.isDirectory); const children = result.children!; - assert.equal(children.length, 4); + assert.strictEqual(children.length, 4); const other = getByName(result, 'other'); assert.ok(other); @@ -345,7 +336,7 @@ suite('Disk File Service', function () { const deep = getByName(other!, 'deep'); assert.ok(deep); assert.ok(deep!.children!.length > 0); - assert.equal(deep!.children!.length, 4); + assert.strictEqual(deep!.children!.length, 4); }); test('resolve directory - resolveTo multiple directories', async () => { @@ -363,7 +354,7 @@ suite('Disk File Service', function () { assert.ok(result.isDirectory); const children = result.children!; - assert.equal(children.length, 4); + assert.strictEqual(children.length, 4); const other = getByName(result, 'other'); assert.ok(other); @@ -372,12 +363,12 @@ suite('Disk File Service', function () { const deep = getByName(other!, 'deep'); assert.ok(deep); assert.ok(deep!.children!.length > 0); - assert.equal(deep!.children!.length, 4); + assert.strictEqual(deep!.children!.length, 4); const examples = getByName(result, 'examples'); assert.ok(examples); assert.ok(examples!.children!.length > 0); - assert.equal(examples!.children!.length, 4); + assert.strictEqual(examples!.children!.length, 4); }); test('resolve directory - resolveSingleChildFolders', async () => { @@ -390,12 +381,12 @@ suite('Disk File Service', function () { assert.ok(result.isDirectory); const children = result.children!; - assert.equal(children.length, 1); + assert.strictEqual(children.length, 1); let deep = getByName(result, 'deep'); assert.ok(deep); assert.ok(deep!.children!.length > 0); - assert.equal(deep!.children!.length, 4); + assert.strictEqual(deep!.children!.length, 4); }); test('resolves', async () => { @@ -405,41 +396,41 @@ suite('Disk File Service', function () { ]); const r1 = (res[0].stat!); - assert.equal(r1.children!.length, 8); + assert.strictEqual(r1.children!.length, 8); const deep = (getByName(r1, 'deep')!); - assert.equal(deep.children!.length, 4); + assert.strictEqual(deep.children!.length, 4); const r2 = (res[1].stat!); - assert.equal(r2.children!.length, 4); - assert.equal(r2.name, 'deep'); + assert.strictEqual(r2.children!.length, 4); + assert.strictEqual(r2.name, 'deep'); }); - (isWindows /* not reliable on windows */ ? test.skip : test)('resolve - folder symbolic link', async () => { + test('resolve - folder symbolic link', async () => { const link = URI.file(join(testDir, 'deep-link')); - await symlink(join(testDir, 'deep'), link.fsPath); + await symlink(join(testDir, 'deep'), link.fsPath, 'junction'); const resolved = await service.resolve(link); - assert.equal(resolved.children!.length, 4); - assert.equal(resolved.isDirectory, true); - assert.equal(resolved.isSymbolicLink, true); + assert.strictEqual(resolved.children!.length, 4); + assert.strictEqual(resolved.isDirectory, true); + assert.strictEqual(resolved.isSymbolicLink, true); }); - (isWindows /* not reliable on windows */ ? test.skip : test)('resolve - file symbolic link', async () => { + (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('resolve - file symbolic link', async () => { const link = URI.file(join(testDir, 'lorem.txt-linked')); await symlink(join(testDir, 'lorem.txt'), link.fsPath); const resolved = await service.resolve(link); - assert.equal(resolved.isDirectory, false); - assert.equal(resolved.isSymbolicLink, true); + assert.strictEqual(resolved.isDirectory, false); + assert.strictEqual(resolved.isSymbolicLink, true); }); - (isWindows /* not reliable on windows */ ? test.skip : test)('resolve - symbolic link pointing to non-existing file does not break', async () => { - await symlink(join(testDir, 'foo'), join(testDir, 'bar')); + test('resolve - symbolic link pointing to non-existing file does not break', async () => { + await symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); const resolved = await service.resolve(URI.file(testDir)); - assert.equal(resolved.isDirectory, true); - assert.equal(resolved.children!.length, 9); + assert.strictEqual(resolved.isDirectory, true); + assert.strictEqual(resolved.children!.length, 9); const resolvedLink = resolved.children?.find(child => child.name === 'bar' && child.isSymbolicLink); assert.ok(resolvedLink); @@ -463,14 +454,14 @@ suite('Disk File Service', function () { const resource = URI.file(join(testDir, 'deep', 'conway.js')); const source = await service.resolve(resource); - assert.equal(await service.canDelete(source.resource, { useTrash }), true); + assert.strictEqual(await service.canDelete(source.resource, { useTrash }), true); await service.del(source.resource, { useTrash }); - assert.equal(existsSync(source.resource.fsPath), false); + assert.strictEqual(existsSync(source.resource.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.DELETE); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); let error: Error | undefined = undefined; try { @@ -480,10 +471,10 @@ suite('Disk File Service', function () { } assert.ok(error); - assert.equal((error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND); + assert.strictEqual((error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND); } - (isWindows /* not reliable on windows */ ? test.skip : test)('deleteFile - symbolic link (exists)', async () => { + (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (exists)', async () => { const target = URI.file(join(testDir, 'lorem.txt')); const link = URI.file(join(testDir, 'lorem.txt-linked')); await symlink(target.fsPath, link.fsPath); @@ -493,19 +484,19 @@ suite('Disk File Service', function () { let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); - assert.equal(await service.canDelete(source.resource), true); + assert.strictEqual(await service.canDelete(source.resource), true); await service.del(source.resource); - assert.equal(existsSync(source.resource.fsPath), false); + assert.strictEqual(existsSync(source.resource.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, link.fsPath); - assert.equal(event!.operation, FileOperation.DELETE); + assert.strictEqual(event!.resource.fsPath, link.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); - assert.equal(existsSync(target.fsPath), true); // target the link pointed to is never deleted + assert.strictEqual(existsSync(target.fsPath), true); // target the link pointed to is never deleted }); - (isWindows /* not reliable on windows */ ? test.skip : test)('deleteFile - symbolic link (pointing to non-existing file)', async () => { + (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (pointing to non-existing file)', async () => { const target = URI.file(join(testDir, 'foo')); const link = URI.file(join(testDir, 'bar')); await symlink(target.fsPath, link.fsPath); @@ -513,14 +504,14 @@ suite('Disk File Service', function () { let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); - assert.equal(await service.canDelete(link), true); + assert.strictEqual(await service.canDelete(link), true); await service.del(link); - assert.equal(existsSync(link.fsPath), false); + assert.strictEqual(existsSync(link.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, link.fsPath); - assert.equal(event!.operation, FileOperation.DELETE); + assert.strictEqual(event!.resource.fsPath, link.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); }); test('deleteFolder (recursive)', async () => { @@ -538,13 +529,13 @@ suite('Disk File Service', function () { const resource = URI.file(join(testDir, 'deep')); const source = await service.resolve(resource); - assert.equal(await service.canDelete(source.resource, { recursive: true, useTrash }), true); + assert.strictEqual(await service.canDelete(source.resource, { recursive: true, useTrash }), true); await service.del(source.resource, { recursive: true, useTrash }); - assert.equal(existsSync(source.resource.fsPath), false); + assert.strictEqual(existsSync(source.resource.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.DELETE); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); } test('deleteFolder (non recursive)', async () => { @@ -572,20 +563,20 @@ suite('Disk File Service', function () { const target = URI.file(join(dirname(source.fsPath), 'other.html')); - assert.equal(await service.canMove(source, target), true); + assert.strictEqual(await service.canMove(source, target), true); const renamed = await service.move(source, target); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); const targetContents = readFileSync(target.fsPath); - assert.equal(sourceContents.byteLength, targetContents.byteLength); - assert.equal(sourceContents.toString(), targetContents.toString()); + assert.strictEqual(sourceContents.byteLength, targetContents.byteLength); + assert.strictEqual(sourceContents.toString(), targetContents.toString()); }); test('move - across providers (buffered => buffered)', async () => { @@ -653,20 +644,20 @@ suite('Disk File Service', function () { const target = URI.file(join(dirname(source.fsPath), 'other.html')).with({ scheme: testSchema }); - assert.equal(await service.canMove(source, target), true); + assert.strictEqual(await service.canMove(source, target), true); const renamed = await service.move(source, target); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); const targetContents = readFileSync(target.fsPath); - assert.equal(sourceContents.byteLength, targetContents.byteLength); - assert.equal(sourceContents.toString(), targetContents.toString()); + assert.strictEqual(sourceContents.byteLength, targetContents.byteLength); + assert.strictEqual(sourceContents.toString(), targetContents.toString()); } test('move - multi folder', async () => { @@ -678,15 +669,15 @@ suite('Disk File Service', function () { const source = URI.file(join(testDir, 'index.html')); - assert.equal(await service.canMove(source, URI.file(join(dirname(source.fsPath), renameToPath))), true); + assert.strictEqual(await service.canMove(source, URI.file(join(dirname(source.fsPath), renameToPath))), true); const renamed = await service.move(source, URI.file(join(dirname(source.fsPath), renameToPath))); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); }); test('move - directory', async () => { @@ -695,15 +686,15 @@ suite('Disk File Service', function () { const source = URI.file(join(testDir, 'deep')); - assert.equal(await service.canMove(source, URI.file(join(dirname(source.fsPath), 'deeper'))), true); + assert.strictEqual(await service.canMove(source, URI.file(join(dirname(source.fsPath), 'deeper'))), true); const renamed = await service.move(source, URI.file(join(dirname(source.fsPath), 'deeper'))); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); }); test('move - directory - across providers (buffered => buffered)', async () => { @@ -743,20 +734,20 @@ suite('Disk File Service', function () { const target = URI.file(join(dirname(source.fsPath), 'deeper')).with({ scheme: testSchema }); - assert.equal(await service.canMove(source, target), true); + assert.strictEqual(await service.canMove(source, target), true); const renamed = await service.move(source, target); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); const targetChildren = readdirSync(target.fsPath); - assert.equal(sourceChildren.length, targetChildren.length); + assert.strictEqual(sourceChildren.length, targetChildren.length); for (let i = 0; i < sourceChildren.length; i++) { - assert.equal(sourceChildren[i], targetChildren[i]); + assert.strictEqual(sourceChildren[i], targetChildren[i]); } } @@ -768,18 +759,18 @@ suite('Disk File Service', function () { assert.ok(source.size > 0); const renamedResource = URI.file(join(dirname(source.resource.fsPath), 'INDEX.html')); - assert.equal(await service.canMove(source.resource, renamedResource), true); + assert.strictEqual(await service.canMove(source.resource, renamedResource), true); let renamed = await service.move(source.resource, renamedResource); - assert.equal(existsSync(renamedResource.fsPath), true); - assert.equal(basename(renamedResource.fsPath), 'INDEX.html'); + assert.strictEqual(existsSync(renamedResource.fsPath), true); + assert.strictEqual(basename(renamedResource.fsPath), 'INDEX.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamedResource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamedResource.fsPath); renamed = await service.resolve(renamedResource, { resolveMetadata: true }); - assert.equal(source.size, renamed.size); + assert.strictEqual(source.size, renamed.size); }); test('move - same file', async () => { @@ -789,18 +780,18 @@ suite('Disk File Service', function () { const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); assert.ok(source.size > 0); - assert.equal(await service.canMove(source.resource, URI.file(source.resource.fsPath)), true); + assert.strictEqual(await service.canMove(source.resource, URI.file(source.resource.fsPath)), true); let renamed = await service.move(source.resource, URI.file(source.resource.fsPath)); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(basename(renamed.resource.fsPath), 'index.html'); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(basename(renamed.resource.fsPath), 'index.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); renamed = await service.resolve(renamed.resource, { resolveMetadata: true }); - assert.equal(source.size, renamed.size); + assert.strictEqual(source.size, renamed.size); }); test('move - same file #2', async () => { @@ -813,18 +804,18 @@ suite('Disk File Service', function () { const targetParent = URI.file(testDir); const target = targetParent.with({ path: posix.join(targetParent.path, posix.basename(source.resource.path)) }); - assert.equal(await service.canMove(source.resource, target), true); + assert.strictEqual(await service.canMove(source.resource, target), true); let renamed = await service.move(source.resource, target); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(basename(renamed.resource.fsPath), 'index.html'); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(basename(renamed.resource.fsPath), 'index.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); renamed = await service.resolve(renamed.resource, { resolveMetadata: true }); - assert.equal(source.size, renamed.size); + assert.strictEqual(source.size, renamed.size); }); test('move - source parent of target', async () => { @@ -848,7 +839,7 @@ suite('Disk File Service', function () { assert.ok(!event!); source = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(originalSize, source.size); + assert.strictEqual(originalSize, source.size); }); test('move - FILE_MOVE_CONFLICT', async () => { @@ -868,11 +859,11 @@ suite('Disk File Service', function () { error = e; } - assert.equal(error.fileOperationResult, FileOperationResult.FILE_MOVE_CONFLICT); + assert.strictEqual(error.fileOperationResult, FileOperationResult.FILE_MOVE_CONFLICT); assert.ok(!event!); source = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(originalSize, source.size); + assert.strictEqual(originalSize, source.size); }); test('move - overwrite folder with file', async () => { @@ -894,17 +885,17 @@ suite('Disk File Service', function () { const f = await service.createFolder(folderResource); const source = URI.file(join(testDir, 'deep', 'conway.js')); - assert.equal(await service.canMove(source, f.resource, true), true); + assert.strictEqual(await service.canMove(source, f.resource, true), true); const moved = await service.move(source, f.resource, true); - assert.equal(existsSync(moved.resource.fsPath), true); + assert.strictEqual(existsSync(moved.resource.fsPath), true); assert.ok(statSync(moved.resource.fsPath).isFile); assert.ok(createEvent!); assert.ok(deleteEvent!); assert.ok(moveEvent!); - assert.equal(moveEvent!.resource.fsPath, source.fsPath); - assert.equal(moveEvent!.target!.resource.fsPath, moved.resource.fsPath); - assert.equal(deleteEvent!.resource.fsPath, folderResource.fsPath); + assert.strictEqual(moveEvent!.resource.fsPath, source.fsPath); + assert.strictEqual(moveEvent!.target!.resource.fsPath, moved.resource.fsPath); + assert.strictEqual(deleteEvent!.resource.fsPath, folderResource.fsPath); }); test('copy', async () => { @@ -949,21 +940,21 @@ suite('Disk File Service', function () { const source = await service.resolve(URI.file(join(testDir, sourceName))); const target = URI.file(join(testDir, 'other.html')); - assert.equal(await service.canCopy(source.resource, target), true); + assert.strictEqual(await service.canCopy(source.resource, target), true); const copied = await service.copy(source.resource, target); - assert.equal(existsSync(copied.resource.fsPath), true); - assert.equal(existsSync(source.resource.fsPath), true); + assert.strictEqual(existsSync(copied.resource.fsPath), true); + assert.strictEqual(existsSync(source.resource.fsPath), true); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, copied.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, copied.resource.fsPath); const sourceContents = readFileSync(source.resource.fsPath); const targetContents = readFileSync(target.fsPath); - assert.equal(sourceContents.byteLength, targetContents.byteLength); - assert.equal(sourceContents.toString(), targetContents.toString()); + assert.strictEqual(sourceContents.byteLength, targetContents.byteLength); + assert.strictEqual(sourceContents.toString(), targetContents.toString()); } test('copy - overwrite folder with file', async () => { @@ -985,17 +976,17 @@ suite('Disk File Service', function () { const f = await service.createFolder(folderResource); const source = URI.file(join(testDir, 'deep', 'conway.js')); - assert.equal(await service.canCopy(source, f.resource, true), true); + assert.strictEqual(await service.canCopy(source, f.resource, true), true); const copied = await service.copy(source, f.resource, true); - assert.equal(existsSync(copied.resource.fsPath), true); + assert.strictEqual(existsSync(copied.resource.fsPath), true); assert.ok(statSync(copied.resource.fsPath).isFile); assert.ok(createEvent!); assert.ok(deleteEvent!); assert.ok(copyEvent!); - assert.equal(copyEvent!.resource.fsPath, source.fsPath); - assert.equal(copyEvent!.target!.resource.fsPath, copied.resource.fsPath); - assert.equal(deleteEvent!.resource.fsPath, folderResource.fsPath); + assert.strictEqual(copyEvent!.resource.fsPath, source.fsPath); + assert.strictEqual(copyEvent!.target!.resource.fsPath, copied.resource.fsPath); + assert.strictEqual(deleteEvent!.resource.fsPath, folderResource.fsPath); }); test('copy - MIX CASE same target - no overwrite', async () => { @@ -1017,17 +1008,17 @@ suite('Disk File Service', function () { if (isLinux) { assert.ok(!error); - assert.equal(canCopy, true); + assert.strictEqual(canCopy, true); - assert.equal(existsSync(copied!.resource.fsPath), true); + assert.strictEqual(existsSync(copied!.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'INDEX.html')); - assert.equal(source.size, copied!.size); + assert.strictEqual(source.size, copied!.size); } else { assert.ok(error); assert.ok(canCopy instanceof Error); source = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(originalSize, source.size); + assert.strictEqual(originalSize, source.size); } }); @@ -1050,17 +1041,17 @@ suite('Disk File Service', function () { if (isLinux) { assert.ok(!error); - assert.equal(canCopy, true); + assert.strictEqual(canCopy, true); - assert.equal(existsSync(copied!.resource.fsPath), true); + assert.strictEqual(existsSync(copied!.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'INDEX.html')); - assert.equal(source.size, copied!.size); + assert.strictEqual(source.size, copied!.size); } else { assert.ok(error); assert.ok(canCopy instanceof Error); source = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(originalSize, source.size); + assert.strictEqual(originalSize, source.size); } }); @@ -1069,18 +1060,18 @@ suite('Disk File Service', function () { assert.ok(source1.size > 0); const renamed = await service.move(source1.resource, URI.file(join(dirname(source1.resource.fsPath), 'CONWAY.js'))); - assert.equal(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'CONWAY.js')); - assert.equal(source1.size, renamed.size); + assert.strictEqual(source1.size, renamed.size); const source2 = await service.resolve(URI.file(join(testDir, 'deep', 'conway.js')), { resolveMetadata: true }); const target = URI.file(join(testDir, basename(source2.resource.path))); - assert.equal(await service.canCopy(source2.resource, target, true), true); + assert.strictEqual(await service.canCopy(source2.resource, target, true), true); const res = await service.copy(source2.resource, target, true); - assert.equal(existsSync(res.resource.fsPath), true); + assert.strictEqual(existsSync(res.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'conway.js')); - assert.equal(source2.size, res.size); + assert.strictEqual(source2.size, res.size); }); test('copy - same file', async () => { @@ -1090,18 +1081,18 @@ suite('Disk File Service', function () { const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); assert.ok(source.size > 0); - assert.equal(await service.canCopy(source.resource, URI.file(source.resource.fsPath)), true); + assert.strictEqual(await service.canCopy(source.resource, URI.file(source.resource.fsPath)), true); let copied = await service.copy(source.resource, URI.file(source.resource.fsPath)); - assert.equal(existsSync(copied.resource.fsPath), true); - assert.equal(basename(copied.resource.fsPath), 'index.html'); + assert.strictEqual(existsSync(copied.resource.fsPath), true); + assert.strictEqual(basename(copied.resource.fsPath), 'index.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, copied.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, copied.resource.fsPath); copied = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(source.size, copied.size); + assert.strictEqual(source.size, copied.size); }); test('copy - same file #2', async () => { @@ -1114,18 +1105,18 @@ suite('Disk File Service', function () { const targetParent = URI.file(testDir); const target = targetParent.with({ path: posix.join(targetParent.path, posix.basename(source.resource.path)) }); - assert.equal(await service.canCopy(source.resource, URI.file(target.fsPath)), true); + assert.strictEqual(await service.canCopy(source.resource, URI.file(target.fsPath)), true); let copied = await service.copy(source.resource, URI.file(target.fsPath)); - assert.equal(existsSync(copied.resource.fsPath), true); - assert.equal(basename(copied.resource.fsPath), 'index.html'); + assert.strictEqual(existsSync(copied.resource.fsPath), true); + assert.strictEqual(basename(copied.resource.fsPath), 'index.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, copied.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, copied.resource.fsPath); copied = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(source.size, copied.size); + assert.strictEqual(source.size, copied.size); }); test('readFile - small file - default', () => { @@ -1193,7 +1184,7 @@ suite('Disk File Service', function () { async function testReadFile(resource: URI): Promise { const content = await service.readFile(resource); - assert.equal(content.value.toString(), readFileSync(resource.fsPath)); + assert.strictEqual(content.value.toString(), readFileSync(resource.fsPath).toString()); } test('readFileStream - small file - default', () => { @@ -1221,7 +1212,7 @@ suite('Disk File Service', function () { async function testReadFileStream(resource: URI): Promise { const content = await service.readFileStream(resource); - assert.equal((await streamToBuffer(content.value)).toString(), readFileSync(resource.fsPath)); + assert.strictEqual((await streamToBuffer(content.value)).toString(), readFileSync(resource.fsPath).toString()); } test('readFile - Files are intermingled #38331 - default', async () => { @@ -1260,8 +1251,8 @@ suite('Disk File Service', function () { service.readFile(resource2) ]); - assert.equal(result[0].value.toString(), value1.value.toString()); - assert.equal(result[1].value.toString(), value2.value.toString()); + assert.strictEqual(result[0].value.toString(), value1.value.toString()); + assert.strictEqual(result[1].value.toString(), value2.value.toString()); } test('readFile - from position (ASCII) - default', async () => { @@ -1291,7 +1282,7 @@ suite('Disk File Service', function () { const contents = await service.readFile(resource, { position: 6 }); - assert.equal(contents.value.toString(), 'File'); + assert.strictEqual(contents.value.toString(), 'File'); } test('readFile - from position (with umlaut) - default', async () => { @@ -1321,7 +1312,7 @@ suite('Disk File Service', function () { const contents = await service.readFile(resource, { position: Buffer.from('Small File with Ü').length }); - assert.equal(contents.value.toString(), 'mlaut'); + assert.strictEqual(contents.value.toString(), 'mlaut'); } test('readFile - 3 bytes (ASCII) - default', async () => { @@ -1351,7 +1342,7 @@ suite('Disk File Service', function () { const contents = await service.readFile(resource, { length: 3 }); - assert.equal(contents.value.toString(), 'Sma'); + assert.strictEqual(contents.value.toString(), 'Sma'); } test('readFile - 20000 bytes (large) - default', async () => { @@ -1403,7 +1394,7 @@ suite('Disk File Service', function () { const contents = await service.readFile(resource, { length }); - assert.equal(contents.value.byteLength, length); + assert.strictEqual(contents.value.byteLength, length); } test('readFile - FILE_IS_DIRECTORY', async () => { @@ -1417,7 +1408,7 @@ suite('Disk File Service', function () { } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_IS_DIRECTORY); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_IS_DIRECTORY); }); (isWindows /* error code does not seem to be supported on windows */ ? test.skip : test)('readFile - FILE_NOT_DIRECTORY', async () => { @@ -1431,7 +1422,7 @@ suite('Disk File Service', function () { } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_NOT_DIRECTORY); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_NOT_DIRECTORY); }); test('readFile - FILE_NOT_FOUND', async () => { @@ -1445,7 +1436,7 @@ suite('Disk File Service', function () { } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_NOT_FOUND); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_NOT_FOUND); }); test('readFile - FILE_NOT_MODIFIED_SINCE - default', async () => { @@ -1484,8 +1475,8 @@ suite('Disk File Service', function () { } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE); - assert.equal(fileProvider.totalBytesRead, 0); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE); + assert.strictEqual(fileProvider.totalBytesRead, 0); } test('readFile - FILE_NOT_MODIFIED_SINCE does not fire wrongly - https://github.com/microsoft/vscode/issues/72909', async () => { @@ -1546,26 +1537,26 @@ suite('Disk File Service', function () { } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT); } - (isWindows ? test.skip /* flaky test */ : test)('readFile - FILE_TOO_LARGE - default', async () => { + test('readFile - FILE_TOO_LARGE - default', async () => { return testFileTooLarge(); }); - (isWindows ? test.skip /* flaky test */ : test)('readFile - FILE_TOO_LARGE - buffered', async () => { + test('readFile - FILE_TOO_LARGE - buffered', async () => { setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); return testFileTooLarge(); }); - (isWindows ? test.skip /* flaky test */ : test)('readFile - FILE_TOO_LARGE - unbuffered', async () => { + test('readFile - FILE_TOO_LARGE - unbuffered', async () => { setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite); return testFileTooLarge(); }); - (isWindows ? test.skip /* flaky test */ : test)('readFile - FILE_TOO_LARGE - streamed', async () => { + test('readFile - FILE_TOO_LARGE - streamed', async () => { setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadStream); return testFileTooLarge(); @@ -1590,9 +1581,23 @@ suite('Disk File Service', function () { } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_TOO_LARGE); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_TOO_LARGE); } + (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('readFile - dangling symbolic link - https://github.com/microsoft/vscode/issues/116049', async () => { + const link = URI.file(join(testDir, 'small.js-link')); + await symlink(join(testDir, 'small.js'), link.fsPath); + + let error: FileOperationError | undefined = undefined; + try { + await service.readFile(link); + } catch (err) { + error = err; + } + + assert.ok(error); + }); + test('createFile', async () => { return assertCreateFile(contents => VSBuffer.fromString(contents)); }); @@ -1612,16 +1617,16 @@ suite('Disk File Service', function () { const contents = 'Hello World'; const resource = URI.file(join(testDir, 'test.txt')); - assert.equal(await service.canCreateFile(resource), true); + assert.strictEqual(await service.canCreateFile(resource), true); const fileStat = await service.createFile(resource, converter(contents)); - assert.equal(fileStat.name, 'test.txt'); - assert.equal(existsSync(fileStat.resource.fsPath), true); - assert.equal(readFileSync(fileStat.resource.fsPath), contents); + assert.strictEqual(fileStat.name, 'test.txt'); + assert.strictEqual(existsSync(fileStat.resource.fsPath), true); + assert.strictEqual(readFileSync(fileStat.resource.fsPath).toString(), contents); assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.fsPath, resource.fsPath); } test('createFile (does not overwrite by default)', async () => { @@ -1651,16 +1656,16 @@ suite('Disk File Service', function () { writeFileSync(resource.fsPath, ''); // create file - assert.equal(await service.canCreateFile(resource, { overwrite: true }), true); + assert.strictEqual(await service.canCreateFile(resource, { overwrite: true }), true); const fileStat = await service.createFile(resource, VSBuffer.fromString(contents), { overwrite: true }); - assert.equal(fileStat.name, 'test.txt'); - assert.equal(existsSync(fileStat.resource.fsPath), true); - assert.equal(readFileSync(fileStat.resource.fsPath), contents); + assert.strictEqual(fileStat.name, 'test.txt'); + assert.strictEqual(existsSync(fileStat.resource.fsPath), true); + assert.strictEqual(readFileSync(fileStat.resource.fsPath).toString(), contents); assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.fsPath, resource.fsPath); }); test('writeFile - default', async () => { @@ -1682,13 +1687,13 @@ suite('Disk File Service', function () { async function testWriteFile() { const resource = URI.file(join(testDir, 'small.txt')); - const content = readFileSync(resource.fsPath); - assert.equal(content, 'Small File'); + const content = readFileSync(resource.fsPath).toString(); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; await service.writeFile(resource, VSBuffer.fromString(newContent)); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); } test('writeFile (large file) - default', async () => { @@ -1714,9 +1719,9 @@ suite('Disk File Service', function () { const newContent = content.toString() + content.toString(); const fileStat = await service.writeFile(resource, VSBuffer.fromString(newContent)); - assert.equal(fileStat.name, 'lorem.txt'); + assert.strictEqual(fileStat.name, 'lorem.txt'); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); } test('writeFile - buffered - readonly throws', async () => { @@ -1734,8 +1739,8 @@ suite('Disk File Service', function () { async function testWriteFileReadonlyThrows() { const resource = URI.file(join(testDir, 'small.txt')); - const content = readFileSync(resource.fsPath); - assert.equal(content, 'Small File'); + const content = readFileSync(resource.fsPath).toString(); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; @@ -1757,7 +1762,7 @@ suite('Disk File Service', function () { await Promise.all(['0', '00', '000', '0000', '00000'].map(async offset => { const fileStat = await service.writeFile(resource, VSBuffer.fromString(offset + newContent)); - assert.equal(fileStat.name, 'lorem.txt'); + assert.strictEqual(fileStat.name, 'lorem.txt'); })); const fileContent = readFileSync(resource.fsPath).toString(); @@ -1783,13 +1788,13 @@ suite('Disk File Service', function () { async function testWriteFileReadable() { const resource = URI.file(join(testDir, 'small.txt')); - const content = readFileSync(resource.fsPath); - assert.equal(content, 'Small File'); + const content = readFileSync(resource.fsPath).toString(); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; await service.writeFile(resource, toLineByLineReadable(newContent)); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); } test('writeFile (large file - readable) - default', async () => { @@ -1815,9 +1820,9 @@ suite('Disk File Service', function () { const newContent = content.toString() + content.toString(); const fileStat = await service.writeFile(resource, toLineByLineReadable(newContent)); - assert.equal(fileStat.name, 'lorem.txt'); + assert.strictEqual(fileStat.name, 'lorem.txt'); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); } test('writeFile (stream) - default', async () => { @@ -1841,10 +1846,10 @@ suite('Disk File Service', function () { const target = URI.file(join(testDir, 'small-copy.txt')); const fileStat = await service.writeFile(target, streamToBufferReadableStream(createReadStream(source.fsPath))); - assert.equal(fileStat.name, 'small-copy.txt'); + assert.strictEqual(fileStat.name, 'small-copy.txt'); const targetContents = readFileSync(target.fsPath).toString(); - assert.equal(readFileSync(source.fsPath).toString(), targetContents); + assert.strictEqual(readFileSync(source.fsPath).toString(), targetContents); } test('writeFile (large file - stream) - default', async () => { @@ -1868,10 +1873,10 @@ suite('Disk File Service', function () { const target = URI.file(join(testDir, 'lorem-copy.txt')); const fileStat = await service.writeFile(target, streamToBufferReadableStream(createReadStream(source.fsPath))); - assert.equal(fileStat.name, 'lorem-copy.txt'); + assert.strictEqual(fileStat.name, 'lorem-copy.txt'); const targetContents = readFileSync(target.fsPath).toString(); - assert.equal(readFileSync(source.fsPath).toString(), targetContents); + assert.strictEqual(readFileSync(source.fsPath).toString(), targetContents); } test('writeFile (file is created including parents)', async () => { @@ -1879,9 +1884,9 @@ suite('Disk File Service', function () { const content = 'File is created including parent'; const fileStat = await service.writeFile(resource, VSBuffer.fromString(content)); - assert.equal(fileStat.name, 'newfile.txt'); + assert.strictEqual(fileStat.name, 'newfile.txt'); - assert.equal(readFileSync(resource.fsPath), content); + assert.strictEqual(readFileSync(resource.fsPath).toString(), content); }); test('writeFile (error when folder is encountered)', async () => { @@ -1902,13 +1907,13 @@ suite('Disk File Service', function () { const stat = await service.resolve(resource); - const content = readFileSync(resource.fsPath); - assert.equal(content, 'Small File'); + const content = readFileSync(resource.fsPath).toString(); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime }); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); }); test('writeFile - error when writing to file that has been updated meanwhile', async () => { @@ -1917,7 +1922,7 @@ suite('Disk File Service', function () { const stat = await service.resolve(resource); const content = readFileSync(resource.fsPath).toString(); - assert.equal(content, 'Small File'); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime }); @@ -1936,7 +1941,7 @@ suite('Disk File Service', function () { assert.ok(error); assert.ok(error instanceof FileOperationError); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE); }); test('writeFile - no error when writing to file where size is the same', async () => { @@ -1945,7 +1950,7 @@ suite('Disk File Service', function () { const stat = await service.resolve(resource); const content = readFileSync(resource.fsPath).toString(); - assert.equal(content, 'Small File'); + assert.strictEqual(content, 'Small File'); const newContent = content; // same content await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime }); @@ -2008,73 +2013,72 @@ suite('Disk File Service', function () { const runWatchTests = isLinux; - (runWatchTests ? test : test.skip)('watch - file', done => { + (runWatchTests ? test : test.skip)('watch - file', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); - + const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests && !isWindows /* symbolic links not reliable on windows */ ? test : test.skip)('watch - file symbolic link', async done => { + (runWatchTests && !isWindows /* windows: cannot create file symbolic link without elevated context */ ? test : test.skip)('watch - file symbolic link', async () => { const toWatch = URI.file(join(testDir, 'lorem.txt-linked')); await symlink(join(testDir, 'lorem.txt'), toWatch.fsPath); - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); - + const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file - multiple writes', done => { + (runWatchTests ? test : test.skip)('watch - file - multiple writes', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); - + const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 1'), 0); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 2'), 10); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 3'), 20); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file - delete file', done => { + (runWatchTests ? test : test.skip)('watch - file - delete file', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done); - + const promise = assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]]); setTimeout(() => unlinkSync(toWatch.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file - rename file', done => { + (runWatchTests ? test : test.skip)('watch - file - rename file', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); const toWatchRenamed = URI.file(join(testDir, 'index-watch1-renamed.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done); - + const promise = assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]]); setTimeout(() => renameSync(toWatch.fsPath, toWatchRenamed.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file - rename file (different case)', done => { + (runWatchTests ? test : test.skip)('watch - file - rename file (different case)', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); const toWatchRenamed = URI.file(join(testDir, 'INDEX-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); - if (isLinux) { - assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done); - } else { - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); // case insensitive file system treat this as change - } + const promise = isLinux + ? assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]]) + : assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); // case insensitive file system treat this as change setTimeout(() => renameSync(toWatch.fsPath, toWatchRenamed.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file (atomic save)', function (done) { + (runWatchTests ? test : test.skip)('watch - file (atomic save)', async () => { const toWatch = URI.file(join(testDir, 'index-watch2.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); + const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => { // Simulate atomic save by deleting the file, creating it under different name @@ -2084,79 +2088,81 @@ suite('Disk File Service', function () { writeFileSync(renamed, 'Changes'); renameSync(renamed, toWatch.fsPath); }, 50); + + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - change file', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - change file', async () => { const watchDir = URI.file(join(testDir, 'watch3')); mkdirSync(watchDir.fsPath); const file = URI.file(join(watchDir.fsPath, 'index.html')); writeFileSync(file.fsPath, 'Init'); - assertWatch(watchDir, [[FileChangeType.UPDATED, file]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.UPDATED, file]]); setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - add file', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - add file', async () => { const watchDir = URI.file(join(testDir, 'watch4')); mkdirSync(watchDir.fsPath); const file = URI.file(join(watchDir.fsPath, 'index.html')); - assertWatch(watchDir, [[FileChangeType.ADDED, file]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.ADDED, file]]); setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - delete file', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - delete file', async () => { const watchDir = URI.file(join(testDir, 'watch5')); mkdirSync(watchDir.fsPath); const file = URI.file(join(watchDir.fsPath, 'index.html')); writeFileSync(file.fsPath, 'Init'); - assertWatch(watchDir, [[FileChangeType.DELETED, file]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.DELETED, file]]); setTimeout(() => unlinkSync(file.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - add folder', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - add folder', async () => { const watchDir = URI.file(join(testDir, 'watch6')); mkdirSync(watchDir.fsPath); const folder = URI.file(join(watchDir.fsPath, 'folder')); - assertWatch(watchDir, [[FileChangeType.ADDED, folder]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.ADDED, folder]]); setTimeout(() => mkdirSync(folder.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - delete folder', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - delete folder', async () => { const watchDir = URI.file(join(testDir, 'watch7')); mkdirSync(watchDir.fsPath); const folder = URI.file(join(watchDir.fsPath, 'folder')); mkdirSync(folder.fsPath); - assertWatch(watchDir, [[FileChangeType.DELETED, folder]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.DELETED, folder]]); setTimeout(() => rimrafSync(folder.fsPath), 50); + await promise; }); - (runWatchTests && !isWindows /* symbolic links not reliable on windows */ ? test : test.skip)('watch - folder (non recursive) - symbolic link - change file', async done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - symbolic link - change file', async () => { const watchDir = URI.file(join(testDir, 'deep-link')); - await symlink(join(testDir, 'deep'), watchDir.fsPath); + await symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction'); const file = URI.file(join(watchDir.fsPath, 'index.html')); writeFileSync(file.fsPath, 'Init'); - assertWatch(watchDir, [[FileChangeType.UPDATED, file]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.UPDATED, file]]); setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - rename file', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - rename file', async () => { const watchDir = URI.file(join(testDir, 'watch8')); mkdirSync(watchDir.fsPath); @@ -2165,12 +2171,12 @@ suite('Disk File Service', function () { const fileRenamed = URI.file(join(watchDir.fsPath, 'index-renamed.html')); - assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]]); setTimeout(() => renameSync(file.fsPath, fileRenamed.fsPath), 50); + await promise; }); - (runWatchTests && isLinux /* this test requires a case sensitive file system */ ? test : test.skip)('watch - folder (non recursive) - rename file (different case)', done => { + (runWatchTests && isLinux /* this test requires a case sensitive file system */ ? test : test.skip)('watch - folder (non recursive) - rename file (different case)', async () => { const watchDir = URI.file(join(testDir, 'watch8')); mkdirSync(watchDir.fsPath); @@ -2179,46 +2185,48 @@ suite('Disk File Service', function () { const fileRenamed = URI.file(join(watchDir.fsPath, 'INDEX.html')); - assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]]); setTimeout(() => renameSync(file.fsPath, fileRenamed.fsPath), 50); + await promise; }); - function assertWatch(toWatch: URI, expected: [FileChangeType, URI][], done: MochaDone): void { - const watcherDisposable = service.watch(toWatch); + function assertWatch(toWatch: URI, expected: [FileChangeType, URI][]): Promise { + return new Promise((resolve, reject) => { + const watcherDisposable = service.watch(toWatch); - function toString(type: FileChangeType): string { - switch (type) { - case FileChangeType.ADDED: return 'added'; - case FileChangeType.DELETED: return 'deleted'; - case FileChangeType.UPDATED: return 'updated'; - } - } - - function printEvents(event: FileChangesEvent): string { - return event.changes.map(change => `Change: type ${toString(change.type)} path ${change.resource.toString()}`).join('\n'); - } - - const listenerDisposable = service.onDidFilesChange(event => { - watcherDisposable.dispose(); - listenerDisposable.dispose(); - - try { - assert.equal(event.changes.length, expected.length, `Expected ${expected.length} events, but got ${event.changes.length}. Details (${printEvents(event)})`); - - if (expected.length === 1) { - assert.equal(event.changes[0].type, expected[0][0], `Expected ${toString(expected[0][0])} but got ${toString(event.changes[0].type)}. Details (${printEvents(event)})`); - assert.equal(event.changes[0].resource.fsPath, expected[0][1].fsPath); - } else { - for (const expect of expected) { - assert.equal(hasChange(event.changes, expect[0], expect[1]), true, `Unable to find ${toString(expect[0])} for ${expect[1].fsPath}. Details (${printEvents(event)})`); - } + function toString(type: FileChangeType): string { + switch (type) { + case FileChangeType.ADDED: return 'added'; + case FileChangeType.DELETED: return 'deleted'; + case FileChangeType.UPDATED: return 'updated'; } - - done(); - } catch (error) { - done(error); } + + function printEvents(event: FileChangesEvent): string { + return event.changes.map(change => `Change: type ${toString(change.type)} path ${change.resource.toString()}`).join('\n'); + } + + const listenerDisposable = service.onDidFilesChange(event => { + watcherDisposable.dispose(); + listenerDisposable.dispose(); + + try { + assert.strictEqual(event.changes.length, expected.length, `Expected ${expected.length} events, but got ${event.changes.length}. Details (${printEvents(event)})`); + + if (expected.length === 1) { + assert.strictEqual(event.changes[0].type, expected[0][0], `Expected ${toString(expected[0][0])} but got ${toString(event.changes[0].type)}. Details (${printEvents(event)})`); + assert.strictEqual(event.changes[0].resource.fsPath, expected[0][1].fsPath); + } else { + for (const expect of expected) { + assert.strictEqual(hasChange(event.changes, expect[0], expect[1]), true, `Unable to find ${toString(expect[0])} for ${expect[1].fsPath}. Details (${printEvents(event)})`); + } + } + + resolve(); + } catch (error) { + reject(error); + } + }); }); } @@ -2234,7 +2242,7 @@ suite('Disk File Service', function () { let fd = await fileProvider.open(resource, { create: false }); for (let i = 0; i < 3; i++) { await fileProvider.read(fd, 0, buffer.buffer, 0, 26); - assert.equal(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); + assert.strictEqual(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); } await fileProvider.close(fd); @@ -2245,31 +2253,31 @@ suite('Disk File Service', function () { let posInFile = 0; await fileProvider.read(fd, posInFile, buffer.buffer, 0, 26); - assert.equal(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); + assert.strictEqual(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); posInFile += 26; await fileProvider.read(fd, posInFile, buffer.buffer, 0, 1); - assert.equal(buffer.slice(0, 1).toString(), ','); + assert.strictEqual(buffer.slice(0, 1).toString(), ','); posInFile += 1; await fileProvider.read(fd, posInFile, buffer.buffer, 0, 12); - assert.equal(buffer.slice(0, 12).toString(), ' consectetur'); + assert.strictEqual(buffer.slice(0, 12).toString(), ' consectetur'); posInFile += 12; await fileProvider.read(fd, 98 /* no longer in sequence of posInFile */, buffer.buffer, 0, 9); - assert.equal(buffer.slice(0, 9).toString(), 'fermentum'); + assert.strictEqual(buffer.slice(0, 9).toString(), 'fermentum'); await fileProvider.read(fd, 27, buffer.buffer, 0, 12); - assert.equal(buffer.slice(0, 12).toString(), ' consectetur'); + assert.strictEqual(buffer.slice(0, 12).toString(), ' consectetur'); await fileProvider.read(fd, 26, buffer.buffer, 0, 1); - assert.equal(buffer.slice(0, 1).toString(), ','); + assert.strictEqual(buffer.slice(0, 1).toString(), ','); await fileProvider.read(fd, 0, buffer.buffer, 0, 26); - assert.equal(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); + assert.strictEqual(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); await fileProvider.read(fd, posInFile /* back in sequence */, buffer.buffer, 0, 11); - assert.equal(buffer.slice(0, 11).toString(), ' adipiscing'); + assert.strictEqual(buffer.slice(0, 11).toString(), ' adipiscing'); await fileProvider.close(fd); }); @@ -2289,7 +2297,7 @@ suite('Disk File Service', function () { posInFileWrite += initialContents.byteLength; await fileProvider.read(fdRead, posInFileRead, buffer.buffer, 0, 26); - assert.equal(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); + assert.strictEqual(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); posInFileRead += 26; const contents = VSBuffer.fromString('Hello World'); @@ -2298,19 +2306,19 @@ suite('Disk File Service', function () { posInFileWrite += contents.byteLength; await fileProvider.read(fdRead, posInFileRead, buffer.buffer, 0, contents.byteLength); - assert.equal(buffer.slice(0, contents.byteLength).toString(), 'Hello World'); + assert.strictEqual(buffer.slice(0, contents.byteLength).toString(), 'Hello World'); posInFileRead += contents.byteLength; await fileProvider.write(fdWrite, 6, contents.buffer, 0, contents.byteLength); await fileProvider.read(fdRead, 0, buffer.buffer, 0, 11); - assert.equal(buffer.slice(0, 11).toString(), 'Lorem Hello'); + assert.strictEqual(buffer.slice(0, 11).toString(), 'Lorem Hello'); await fileProvider.write(fdWrite, posInFileWrite, contents.buffer, 0, contents.byteLength); posInFileWrite += contents.byteLength; await fileProvider.read(fdRead, posInFileWrite - contents.byteLength, buffer.buffer, 0, contents.byteLength); - assert.equal(buffer.slice(0, contents.byteLength).toString(), 'Hello World'); + assert.strictEqual(buffer.slice(0, contents.byteLength).toString(), 'Hello World'); await fileProvider.close(fdWrite); await fileProvider.close(fdRead); diff --git a/src/vs/platform/files/test/electron-browser/normalizer.test.ts b/src/vs/platform/files/test/electron-browser/normalizer.test.ts index 98aa1162c..df672c5f0 100644 --- a/src/vs/platform/files/test/electron-browser/normalizer.test.ts +++ b/src/vs/platform/files/test/electron-browser/normalizer.test.ts @@ -64,7 +64,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 3); + assert.strictEqual(e.changes.length, 3); assert.ok(e.contains(added, FileChangeType.ADDED)); assert.ok(e.contains(updated, FileChangeType.UPDATED)); assert.ok(e.contains(deleted, FileChangeType.DELETED)); @@ -103,7 +103,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 5); + assert.strictEqual(e.changes.length, 5); assert.ok(e.contains(deletedFolderA, FileChangeType.DELETED)); assert.ok(e.contains(deletedFolderB, FileChangeType.DELETED)); @@ -133,7 +133,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 1); + assert.strictEqual(e.changes.length, 1); assert.ok(e.contains(unrelated, FileChangeType.UPDATED)); @@ -158,7 +158,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 2); + assert.strictEqual(e.changes.length, 2); assert.ok(e.contains(deleted, FileChangeType.UPDATED)); assert.ok(e.contains(unrelated, FileChangeType.UPDATED)); @@ -184,7 +184,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 2); + assert.strictEqual(e.changes.length, 2); assert.ok(e.contains(created, FileChangeType.ADDED)); assert.ok(!e.contains(created, FileChangeType.UPDATED)); @@ -213,7 +213,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 2); + assert.strictEqual(e.changes.length, 2); assert.ok(e.contains(deleted, FileChangeType.DELETED)); assert.ok(!e.contains(updated, FileChangeType.UPDATED)); diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index 0fd23fd50..00f0cb9d9 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -132,15 +132,31 @@ export class InstantiationService implements IInstantiationService { private _getOrCreateServiceInstance(id: ServiceIdentifier, _trace: Trace): T { let thing = this._getServiceInstanceOrDescriptor(id); if (thing instanceof SyncDescriptor) { - return this._createAndCacheServiceInstance(id, thing, _trace.branch(id, true)); + return this._safeCreateAndCacheServiceInstance(id, thing, _trace.branch(id, true)); } else { _trace.branch(id, false); return thing; } } + private readonly _activeInstantiations = new Set>(); + + + private _safeCreateAndCacheServiceInstance(id: ServiceIdentifier, desc: SyncDescriptor, _trace: Trace): T { + if (this._activeInstantiations.has(id)) { + throw new Error(`illegal state - RECURSIVELY instantiating service '${id}'`); + } + this._activeInstantiations.add(id); + try { + return this._createAndCacheServiceInstance(id, desc, _trace); + } finally { + this._activeInstantiations.delete(id); + } + } + private _createAndCacheServiceInstance(id: ServiceIdentifier, desc: SyncDescriptor, _trace: Trace): T { - type Triple = { id: ServiceIdentifier, desc: SyncDescriptor, _trace: Trace }; + + type Triple = { id: ServiceIdentifier, desc: SyncDescriptor, _trace: Trace; }; const graph = new Graph(data => data.id.toString()); let cycleCount = 0; @@ -195,7 +211,6 @@ export class InstantiationService implements IInstantiationService { graph.removeNode(data); } } - return this._getServiceInstanceOrDescriptor(id); } diff --git a/src/vs/platform/ipc/electron-browser/sharedProcessService.ts b/src/vs/platform/ipc/electron-browser/sharedProcessService.ts index 59fb5a6bd..60299f96e 100644 --- a/src/vs/platform/ipc/electron-browser/sharedProcessService.ts +++ b/src/vs/platform/ipc/electron-browser/sharedProcessService.ts @@ -4,7 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { Event } from 'vs/base/common/event'; +import { IpcRendererEvent } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp'; +import { IChannel, IServerChannel, getDelayedChannel } from 'vs/base/parts/ipc/common/ipc'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Disposable } from 'vs/base/common/lifecycle'; export const ISharedProcessService = createDecorator('sharedProcessService'); @@ -14,7 +22,47 @@ export interface ISharedProcessService { getChannel(channelName: string): IChannel; registerChannel(channelName: string, channel: IServerChannel): void; - - whenSharedProcessReady(): Promise; - toggleSharedProcessWindow(): Promise; +} + +export class SharedProcessService extends Disposable implements ISharedProcessService { + + declare readonly _serviceBrand: undefined; + + private readonly withSharedProcessConnection: Promise; + + constructor( + @INativeHostService private readonly nativeHostService: INativeHostService, + @ILogService private readonly logService: ILogService + ) { + super(); + + this.withSharedProcessConnection = this.connect(); + } + + private async connect(): Promise { + this.logService.trace('Renderer->SharedProcess#connect'); + + // Ask to create message channel inside the window + // and send over a UUID to correlate the response + const nonce = generateUuid(); + ipcRenderer.send('vscode:createSharedProcessMessageChannel', nonce); + + // Wait until the main side has returned the `MessagePort` + // We need to filter by the `nonce` to ensure we listen + // to the right response. + const onMessageChannelResult = Event.fromNodeEventEmitter<{ nonce: string, port: MessagePort }>(ipcRenderer, 'vscode:createSharedProcessMessageChannelResult', (e: IpcRendererEvent, nonce: string) => ({ nonce, port: e.ports[0] })); + const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce))); + + this.logService.trace('Renderer->SharedProcess#connect: connection established'); + + return this._register(new MessagePortClient(port, `window:${this.nativeHostService.windowId}`)); + } + + getChannel(channelName: string): IChannel { + return getDelayedChannel(this.withSharedProcessConnection.then(connection => connection.getChannel(channelName))); + } + + registerChannel(channelName: string, channel: IServerChannel): void { + this.withSharedProcessConnection.then(connection => connection.registerChannel(channelName, channel)); + } } diff --git a/src/vs/platform/ipc/electron-main/sharedProcessMainService.ts b/src/vs/platform/ipc/electron-main/sharedProcessMainService.ts deleted file mode 100644 index 97decd56a..000000000 --- a/src/vs/platform/ipc/electron-main/sharedProcessMainService.ts +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; - -export const ISharedProcessMainService = createDecorator('sharedProcessMainService'); - -export interface ISharedProcessMainService { - - readonly _serviceBrand: undefined; - - whenSharedProcessReady(): Promise; - toggleSharedProcessWindow(): Promise; -} - -export interface ISharedProcess { - whenReady(): Promise; - toggle(): void; -} - -export class SharedProcessMainService implements ISharedProcessMainService { - - declare readonly _serviceBrand: undefined; - - constructor(private sharedProcess: ISharedProcess) { } - - whenSharedProcessReady(): Promise { - return this.sharedProcess.whenReady(); - } - - async toggleSharedProcessWindow(): Promise { - return this.sharedProcess.toggle(); - } -} diff --git a/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts b/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts index b2c78a332..2f326523c 100644 --- a/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts +++ b/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Client } from 'vs/base/parts/ipc/electron-sandbox/ipc.electron-sandbox'; +import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; +import { Client as IPCElectronClient } from 'vs/base/parts/ipc/electron-sandbox/ipc.electron'; import { Disposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp'; export const IMainProcessService = createDecorator('mainProcessService'); @@ -19,18 +20,21 @@ export interface IMainProcessService { registerChannel(channelName: string, channel: IServerChannel): void; } -export class MainProcessService extends Disposable implements IMainProcessService { +/** + * An implementation of `IMainProcessService` that leverages Electron's IPC. + */ +export class ElectronIPCMainProcessService extends Disposable implements IMainProcessService { declare readonly _serviceBrand: undefined; - private mainProcessConnection: Client; + private mainProcessConnection: IPCElectronClient; constructor( windowId: number ) { super(); - this.mainProcessConnection = this._register(new Client(`window:${windowId}`)); + this.mainProcessConnection = this._register(new IPCElectronClient(`window:${windowId}`)); } getChannel(channelName: string): IChannel { @@ -41,3 +45,24 @@ export class MainProcessService extends Disposable implements IMainProcessServic this.mainProcessConnection.registerChannel(channelName, channel); } } + +/** + * An implementation of `IMainProcessService` that leverages MessagePorts. + */ +export class MessagePortMainProcessService implements IMainProcessService { + + declare readonly _serviceBrand: undefined; + + constructor( + private server: MessagePortServer, + private router: StaticRouter + ) { } + + getChannel(channelName: string): IChannel { + return this.server.getChannel(channelName, this.router); + } + + registerChannel(channelName: string, channel: IServerChannel): void { + this.server.registerChannel(channelName, channel); + } +} diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index e981a34d5..fc36369c6 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -56,6 +56,7 @@ export interface IssueReporterData extends WindowData { issueType?: IssueType; extensionId?: string; experiments?: string; + githubAccessToken: string; readonly issueTitle?: string; readonly issueBody?: string; } @@ -72,7 +73,6 @@ export interface IssueReporterFeatures { export interface ProcessExplorerStyles extends WindowStyles { hoverBackground?: string; hoverForeground?: string; - highlightForeground?: string; } export interface ProcessExplorerData extends WindowData { diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index da98e2f94..994f4559d 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -17,7 +17,7 @@ import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowState } from 'vs/platform/windows/electron-main/windows'; import { listProcesses } from 'vs/base/node/ps'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { zoomLevelToZoomFactor } from 'vs/platform/windows/common/windows'; import { FileAccess } from 'vs/base/common/network'; @@ -185,120 +185,118 @@ export class IssueMainService implements ICommonIssueService { } } - openReporter(data: IssueReporterData): Promise { - return new Promise(_ => { - if (!this._issueWindow) { - this._issueParentWindow = BrowserWindow.getFocusedWindow(); - if (this._issueParentWindow) { - const position = this.getWindowPosition(this._issueParentWindow, 700, 800); + async openReporter(data: IssueReporterData): Promise { + if (!this._issueWindow) { + this._issueParentWindow = BrowserWindow.getFocusedWindow(); + if (this._issueParentWindow) { + const position = this.getWindowPosition(this._issueParentWindow, 700, 800); - this._issueWindow = new BrowserWindow({ - fullscreen: false, - width: position.width, - height: position.height, - minWidth: 300, - minHeight: 200, - x: position.x, - y: position.y, - title: localize('issueReporter', "Issue Reporter"), - backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR, - webPreferences: { - preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, - enableWebSQL: false, - enableRemoteModule: false, - spellcheck: false, - nativeWindowOpen: true, - zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), - sandbox: true, - contextIsolation: true - } - }); + this._issueWindow = new BrowserWindow({ + fullscreen: false, + width: position.width, + height: position.height, + minWidth: 300, + minHeight: 200, + x: position.x, + y: position.y, + title: localize('issueReporter', "Issue Reporter"), + backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR, + webPreferences: { + preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, + v8CacheOptions: 'bypassHeatCheck', + enableWebSQL: false, + enableRemoteModule: false, + spellcheck: false, + nativeWindowOpen: true, + zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), + sandbox: true, + contextIsolation: true + } + }); - this._issueWindow.setMenuBarVisibility(false); // workaround for now, until a menu is implemented + this._issueWindow.setMenuBarVisibility(false); // workaround for now, until a menu is implemented - // Modified when testing UI - const features: IssueReporterFeatures = {}; + // Modified when testing UI + const features: IssueReporterFeatures = {}; - this.logService.trace('issueService#openReporter: opening issue reporter'); - this._issueWindow.loadURL(this.getIssueReporterPath(data, features)); + this.logService.trace('issueService#openReporter: opening issue reporter'); + this._issueWindow.loadURL(this.getIssueReporterPath(data, features)); - this._issueWindow.on('close', () => this._issueWindow = null); + this._issueWindow.on('close', () => this._issueWindow = null); - this._issueParentWindow.on('closed', () => { - if (this._issueWindow) { - this._issueWindow.close(); - this._issueWindow = null; - } - }); - } + this._issueParentWindow.on('closed', () => { + if (this._issueWindow) { + this._issueWindow.close(); + this._issueWindow = null; + } + }); } + } - if (this._issueWindow) { - this._issueWindow.focus(); - } - }); + if (this._issueWindow) { + this._issueWindow.focus(); + } } - openProcessExplorer(data: ProcessExplorerData): Promise { - return new Promise(_ => { - // Create as singleton - if (!this._processExplorerWindow) { - this._processExplorerParentWindow = BrowserWindow.getFocusedWindow(); - if (this._processExplorerParentWindow) { - const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 500); - this._processExplorerWindow = new BrowserWindow({ - skipTaskbar: true, - resizable: true, - fullscreen: false, - width: position.width, - height: position.height, - minWidth: 300, - minHeight: 200, - x: position.x, - y: position.y, - backgroundColor: data.styles.backgroundColor, - title: localize('processExplorer', "Process Explorer"), - webPreferences: { - preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, - enableWebSQL: false, - enableRemoteModule: false, - spellcheck: false, - nativeWindowOpen: true, - zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), - sandbox: true, - contextIsolation: true - } - }); + async openProcessExplorer(data: ProcessExplorerData): Promise { + // Create as singleton + if (!this._processExplorerWindow) { + this._processExplorerParentWindow = BrowserWindow.getFocusedWindow(); + if (this._processExplorerParentWindow) { + const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 500); + this._processExplorerWindow = new BrowserWindow({ + skipTaskbar: true, + resizable: true, + fullscreen: false, + width: position.width, + height: position.height, + minWidth: 300, + minHeight: 200, + x: position.x, + y: position.y, + backgroundColor: data.styles.backgroundColor, + title: localize('processExplorer', "Process Explorer"), + webPreferences: { + preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, + v8CacheOptions: 'bypassHeatCheck', + enableWebSQL: false, + enableRemoteModule: false, + spellcheck: false, + nativeWindowOpen: true, + zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), + sandbox: true, + contextIsolation: true + } + }); - this._processExplorerWindow.setMenuBarVisibility(false); + this._processExplorerWindow.setMenuBarVisibility(false); - const windowConfiguration = { - appRoot: this.environmentService.appRoot, - windowId: this._processExplorerWindow.id, - userEnv: this.userEnv, - machineId: this.machineId, - data - }; + const windowConfiguration = { + appRoot: this.environmentService.appRoot, + windowId: this._processExplorerWindow.id, + userEnv: this.userEnv, + machineId: this.machineId, + data + }; - this._processExplorerWindow.loadURL( - toWindowUrl('vs/code/electron-sandbox/processExplorer/processExplorer.html', windowConfiguration)); + this._processExplorerWindow.loadURL( + toWindowUrl('vs/code/electron-sandbox/processExplorer/processExplorer.html', windowConfiguration)); - this._processExplorerWindow.on('close', () => this._processExplorerWindow = null); + this._processExplorerWindow.on('close', () => this._processExplorerWindow = null); - this._processExplorerParentWindow.on('close', () => { - if (this._processExplorerWindow) { - this._processExplorerWindow.close(); - this._processExplorerWindow = null; - } - }); - } + this._processExplorerParentWindow.on('close', () => { + if (this._processExplorerWindow) { + this._processExplorerWindow.close(); + this._processExplorerWindow = null; + } + }); } + } - // Focus - if (this._processExplorerWindow) { - this._processExplorerWindow.focus(); - } - }); + // Focus + if (this._processExplorerWindow) { + this._processExplorerWindow.focus(); + } } public async getSystemStatus(): Promise { @@ -414,7 +412,7 @@ export class IssueMainService implements ICommonIssueService { }, product: { nameShort: product.nameShort, - version: product.version, + version: !!product.darwinUniversalAssetId ? `${product.version} (Universal)` : product.version, commit: product.commit, date: product.date, reportIssueUrl: product.reportIssueUrl @@ -436,7 +434,7 @@ function toWindowUrl(modulePathToHtml: string, windowConfiguration: T): strin } return FileAccess - ._asCodeFileUri(modulePathToHtml, require) + .asBrowserUri(modulePathToHtml, require, true) .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }) .toString(true); } diff --git a/src/vs/platform/keybinding/common/keybindingResolver.ts b/src/vs/platform/keybinding/common/keybindingResolver.ts index 3582ce6d6..ca31e62e5 100644 --- a/src/vs/platform/keybinding/common/keybindingResolver.ts +++ b/src/vs/platform/keybinding/common/keybindingResolver.ts @@ -3,9 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { MenuRegistry } from 'vs/platform/actions/common/actions'; -import { CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { IContext, ContextKeyExpression, ContextKeyExprType } from 'vs/platform/contextkey/common/contextkey'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; @@ -340,39 +337,6 @@ export class KeybindingResolver { } return rules.evaluate(context); } - - public static getAllUnboundCommands(boundCommands: Map): string[] { - const unboundCommands: string[] = []; - const seenMap: Map = new Map(); - const addCommand = (id: string, includeCommandWithArgs: boolean) => { - if (seenMap.has(id)) { - return; - } - seenMap.set(id, true); - if (id[0] === '_' || id.indexOf('vscode.') === 0) { // private command - return; - } - if (boundCommands.get(id) === true) { - return; - } - if (!includeCommandWithArgs) { - const command = CommandsRegistry.getCommand(id); - if (command && typeof command.description === 'object' - && isNonEmptyArray((command.description).args)) { // command with args - return; - } - } - unboundCommands.push(id); - }; - for (const id of MenuRegistry.getCommands().keys()) { - addCommand(id, true); - } - for (const id of CommandsRegistry.getCommands().keys()) { - addCommand(id, false); - } - - return unboundCommands; - } } function printWhenExplanation(when: ContextKeyExpression | undefined): string { diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index cbbb54344..d627e6626 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -211,13 +211,13 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + K let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, [ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, [ `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` ]); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -225,13 +225,13 @@ suite('AbstractKeybindingService', () => { // send backspace shouldPreventDefault = kbService.testDispatch(KeyCode.Backspace); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, [ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, [ `The key combination (${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}, ${toUsLabel(KeyCode.Backspace)}) is not a command.` ]); - assert.deepEqual(statusMessageCallsDisposed, [ + assert.deepStrictEqual(statusMessageCallsDisposed, [ `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` ]); executeCommandCalls = []; @@ -241,14 +241,14 @@ suite('AbstractKeybindingService', () => { // send backspace shouldPreventDefault = kbService.testDispatch(KeyCode.Backspace); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -273,11 +273,11 @@ suite('AbstractKeybindingService', () => { function assertIsIgnored(keybinding: number): void { let shouldPreventDefault = kbService.testDispatch(keybinding); - assert.equal(shouldPreventDefault, false); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.strictEqual(shouldPreventDefault, false); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -310,14 +310,14 @@ suite('AbstractKeybindingService', () => { key1: true }); let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -326,13 +326,13 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + K currentContextValue = createContext({}); shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, [ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, [ `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` ]); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -341,14 +341,14 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + X currentContextValue = createContext({}); shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_X); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'chordCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, [ + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, [ `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` ]); executeCommandCalls = []; @@ -370,14 +370,14 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + K currentContextValue = createContext({}); let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -388,14 +388,14 @@ suite('AbstractKeybindingService', () => { key1: true }); shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -406,11 +406,11 @@ suite('AbstractKeybindingService', () => { key1: true }); shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_X); - assert.equal(shouldPreventDefault, false); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.strictEqual(shouldPreventDefault, false); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -428,14 +428,14 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + K currentContextValue = createContext({}); let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, false); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, false); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; diff --git a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts index a321d9b38..f1ef51b64 100644 --- a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts @@ -11,7 +11,7 @@ suite('KeybindingLabels', () => { function assertUSLabel(OS: OperatingSystem, keybinding: number, expected: string): void { const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS); - assert.equal(usResolvedKeybinding.getLabel(), expected); + assert.strictEqual(usResolvedKeybinding.getLabel(), expected); } test('Windows US label', () => { @@ -116,7 +116,7 @@ suite('KeybindingLabels', () => { test('Aria label', () => { function assertAriaLabel(OS: OperatingSystem, keybinding: number, expected: string): void { const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS); - assert.equal(usResolvedKeybinding.getAriaLabel(), expected); + assert.strictEqual(usResolvedKeybinding.getAriaLabel(), expected); } assertAriaLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Windows+A'); @@ -127,7 +127,7 @@ suite('KeybindingLabels', () => { test('Electron Accelerator label', () => { function assertElectronAcceleratorLabel(OS: OperatingSystem, keybinding: number, expected: string | null): void { const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS); - assert.equal(usResolvedKeybinding.getElectronAccelerator(), expected); + assert.strictEqual(usResolvedKeybinding.getElectronAccelerator(), expected); } assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Super+A'); @@ -154,7 +154,7 @@ suite('KeybindingLabels', () => { test('User Settings label', () => { function assertElectronAcceleratorLabel(OS: OperatingSystem, keybinding: number, expected: string): void { const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS); - assert.equal(usResolvedKeybinding.getUserSettingsLabel(), expected); + assert.strictEqual(usResolvedKeybinding.getUserSettingsLabel(), expected); } assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'ctrl+shift+alt+win+a'); diff --git a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts index 2820e0ea4..33472df8a 100644 --- a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts @@ -43,12 +43,12 @@ suite('KeybindingResolver', () => { let contextRules = ContextKeyExpr.equals('bar', 'baz'); let keybindingItem = kbItem(keybinding, 'yes', null, contextRules, true); - assert.equal(KeybindingResolver.contextMatchesRules(createContext({ bar: 'baz' }), contextRules), true); - assert.equal(KeybindingResolver.contextMatchesRules(createContext({ bar: 'bz' }), contextRules), false); + assert.strictEqual(KeybindingResolver.contextMatchesRules(createContext({ bar: 'baz' }), contextRules), true); + assert.strictEqual(KeybindingResolver.contextMatchesRules(createContext({ bar: 'bz' }), contextRules), false); let resolver = new KeybindingResolver([keybindingItem], [], () => { }); - assert.equal(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandId, 'yes'); - assert.equal(resolver.resolve(createContext({ bar: 'bz' }), null, getDispatchStr(runtimeKeybinding)), null); + assert.strictEqual(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandId, 'yes'); + assert.strictEqual(resolver.resolve(createContext({ bar: 'bz' }), null, getDispatchStr(runtimeKeybinding)), null); }); test('resolve key with arguments', function () { @@ -59,7 +59,7 @@ suite('KeybindingResolver', () => { let keybindingItem = kbItem(keybinding, 'yes', commandArgs, contextRules, true); let resolver = new KeybindingResolver([keybindingItem], [], () => { }); - assert.equal(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandArgs, commandArgs); + assert.strictEqual(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandArgs, commandArgs); }); test('KeybindingResolver.combine simple 1', function () { @@ -70,7 +70,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false), ]); @@ -85,7 +85,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_C, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true), kbItem(KeyCode.KEY_C, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false), @@ -101,7 +101,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_A, '-yes1', null, ContextKeyExpr.equals('1', 'b'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); @@ -116,7 +116,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_B, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); @@ -131,7 +131,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_A, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); @@ -145,7 +145,7 @@ suite('KeybindingResolver', () => { kbItem(0, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); @@ -159,7 +159,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_A, '-yes1', null, null!, false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); @@ -173,7 +173,7 @@ suite('KeybindingResolver', () => { kbItem(0, '-yes1', null, null!, false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); @@ -187,17 +187,17 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_A, '-yes1', null, null!, false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); test('contextIsEntirelyIncluded', () => { const assertIsIncluded = (a: string | null, b: string | null) => { - assert.equal(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), true); + assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), true); }; const assertIsNotIncluded = (a: string | null, b: string | null) => { - assert.equal(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), false); + assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), false); }; assertIsIncluded('key1', null); @@ -314,11 +314,11 @@ suite('KeybindingResolver', () => { let testKey = (commandId: string, expectedKeys: number[]) => { // Test lookup let lookupResult = resolver.lookupKeybindings(commandId); - assert.equal(lookupResult.length, expectedKeys.length, 'Length mismatch @ commandId ' + commandId + '; GOT: ' + JSON.stringify(lookupResult, null, '\t')); + assert.strictEqual(lookupResult.length, expectedKeys.length, 'Length mismatch @ commandId ' + commandId + '; GOT: ' + JSON.stringify(lookupResult, null, '\t')); for (let i = 0, len = lookupResult.length; i < len; i++) { const expected = new USLayoutResolvedKeybinding(createKeybinding(expectedKeys[i], OS)!, OS); - assert.equal(lookupResult[i].resolvedKeybinding!.getUserSettingsLabel(), expected.getUserSettingsLabel(), 'value mismatch @ commandId ' + commandId); + assert.strictEqual(lookupResult[i].resolvedKeybinding!.getUserSettingsLabel(), expected.getUserSettingsLabel(), 'value mismatch @ commandId ' + commandId); } }; @@ -333,14 +333,14 @@ suite('KeybindingResolver', () => { // if it's the final part, then we should find a valid command, // and there should not be a chord. assert.ok(result !== null, `Enters chord for ${commandId} at part ${i}`); - assert.equal(result!.commandId, commandId, `Enters chord for ${commandId} at part ${i}`); - assert.equal(result!.enterChord, false, `Enters chord for ${commandId} at part ${i}`); + assert.strictEqual(result!.commandId, commandId, `Enters chord for ${commandId} at part ${i}`); + assert.strictEqual(result!.enterChord, false, `Enters chord for ${commandId} at part ${i}`); } else { // if it's not the final part, then we should not find a valid command, // and there should be a chord. assert.ok(result !== null, `Enters chord for ${commandId} at part ${i}`); - assert.equal(result!.commandId, null, `Enters chord for ${commandId} at part ${i}`); - assert.equal(result!.enterChord, true, `Enters chord for ${commandId} at part ${i}`); + assert.strictEqual(result!.commandId, null, `Enters chord for ${commandId} at part ${i}`); + assert.strictEqual(result!.enterChord, true, `Enters chord for ${commandId} at part ${i}`); } previousPart = part; } diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index d1739d0a6..e9c0c56e7 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { IWorkspace } from 'vs/platform/workspace/common/workspace'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; export const ILabelService = createDecorator('labelService'); @@ -23,7 +23,7 @@ export interface ILabelService { */ getUriLabel(resource: URI, options?: { relative?: boolean, noPrefix?: boolean, endWithSeparator?: boolean }): string; getUriBasenameLabel(resource: URI): string; - getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWorkspace), options?: { verbose: boolean }): string; + getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | IWorkspace), options?: { verbose: boolean }): string; getHostLabel(scheme: string, authority?: string): string; getSeparator(scheme: string, authority?: string): '/' | '\\'; diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 9b3d65663..0ede527e5 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -9,10 +9,9 @@ import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWindowSettings } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; -import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { whenDeleted } from 'vs/base/node/pfs'; -import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; import { BrowserWindow, ipcMain, Event as IpcEvent, app } from 'electron'; @@ -21,6 +20,7 @@ import { IDiagnosticInfoOptions, IDiagnosticInfo, IRemoteDiagnosticInfo, IRemote import { IMainProcessInfo, IWindowInfo } from 'vs/platform/launch/node/launch'; import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; export const ID = 'launchMainService'; export const ILaunchMainService = createDecorator(ID); @@ -35,23 +35,6 @@ export interface IRemoteDiagnosticOptions { includeWorkspaceMetadata?: boolean; } -function parseOpenUrl(args: NativeParsedArgs): { uri: URI, url: string }[] { - if (args['open-url'] && args._urls && args._urls.length > 0) { - // --open-url must contain -- followed by the url(s) - // process.argv is used over args._ as args._ are resolved to file paths at this point - return coalesce(args._urls - .map(url => { - try { - return { uri: URI.parse(url), url }; - } catch (err) { - return null; - } - })); - } - - return []; -} - export interface ILaunchMainService { readonly _serviceBrand: undefined; start(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise; @@ -68,7 +51,7 @@ export class LaunchMainService implements ILaunchMainService { @ILogService private readonly logService: ILogService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IURLService private readonly urlService: IURLService, - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @IConfigurationService private readonly configurationService: IConfigurationService ) { } @@ -89,7 +72,7 @@ export class LaunchMainService implements ILaunchMainService { } // Check early for open-url which is handled in URL service - const urlsToOpen = parseOpenUrl(args); + const urlsToOpen = this.parseOpenUrl(args); if (urlsToOpen.length) { let whenWindowReady: Promise = Promise.resolve(); @@ -113,7 +96,24 @@ export class LaunchMainService implements ILaunchMainService { } } - private startOpenWindow(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise { + private parseOpenUrl(args: NativeParsedArgs): { uri: URI, url: string }[] { + if (args['open-url'] && args._urls && args._urls.length > 0) { + // --open-url must contain -- followed by the url(s) + // process.argv is used over args._ as args._ are resolved to file paths at this point + return coalesce(args._urls + .map(url => { + try { + return { uri: URI.parse(url), url }; + } catch (err) { + return null; + } + })); + } + + return []; + } + + private async startOpenWindow(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise { const context = isLaunchedFromCli(userEnv) ? OpenContext.CLI : OpenContext.DESKTOP; let usedWindows: ICodeWindow[] = []; @@ -205,17 +205,15 @@ export class LaunchMainService implements ILaunchMainService { whenDeleted(waitMarkerFileURI.fsPath) ]).then(() => undefined, () => undefined); } - - return Promise.resolve(undefined); } - getMainProcessId(): Promise { + async getMainProcessId(): Promise { this.logService.trace('Received request for process ID from other instance.'); - return Promise.resolve(process.pid); + return process.pid; } - getMainProcessInfo(): Promise { + async getMainProcessInfo(): Promise { this.logService.trace('Received request for main process info from other instance.'); const windows: IWindowInfo[] = []; @@ -228,18 +226,18 @@ export class LaunchMainService implements ILaunchMainService { } }); - return Promise.resolve({ + return { mainPID: process.pid, mainArguments: process.argv.slice(1), windows, screenReader: !!app.accessibilitySupportEnabled, gpuFeatureStatus: app.getGPUFeatureStatus() - }); + }; } - getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> { + async getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> { const windows = this.windowsMainService.getWindows(); - const promises: Promise[] = windows.map(window => { + const diagnostics: Array = await Promise.all(windows.map(window => { return new Promise((resolve) => { const remoteAuthority = window.remoteAuthority; if (remoteAuthority) { @@ -267,27 +265,26 @@ export class LaunchMainService implements ILaunchMainService { resolve(undefined); } }); - }); + })); - return Promise.all(promises).then(diagnostics => diagnostics.filter((x): x is IRemoteDiagnosticInfo | IRemoteDiagnosticError => !!x)); + return diagnostics.filter((x): x is IRemoteDiagnosticInfo | IRemoteDiagnosticError => !!x); } private getFolderURIs(window: ICodeWindow): URI[] { const folderURIs: URI[] = []; - if (window.openedFolderUri) { - folderURIs.push(window.openedFolderUri); - } else if (window.openedWorkspace) { - // workspace folders can only be shown for local workspaces - const workspaceConfigPath = window.openedWorkspace.configPath; - const resolvedWorkspace = this.workspacesMainService.resolveLocalWorkspaceSync(workspaceConfigPath); + const workspace = window.openedWorkspace; + if (isSingleFolderWorkspaceIdentifier(workspace)) { + folderURIs.push(workspace.uri); + } else if (isWorkspaceIdentifier(workspace)) { + const resolvedWorkspace = this.workspacesManagementMainService.resolveLocalWorkspaceSync(workspace.configPath); // workspace folders can only be shown for local (resolved) workspaces if (resolvedWorkspace) { const rootFolders = resolvedWorkspace.folders; rootFolders.forEach(root => { folderURIs.push(root.uri); }); } else { - //TODO: can we add the workspace file here? + //TODO@RMacfarlane: can we add the workspace file here? } } @@ -296,13 +293,14 @@ export class LaunchMainService implements ILaunchMainService { private codeWindowToInfo(window: ICodeWindow): IWindowInfo { const folderURIs = this.getFolderURIs(window); + return this.browserWindowToInfo(window.win, folderURIs, window.remoteAuthority); } - private browserWindowToInfo(win: BrowserWindow, folderURIs: URI[] = [], remoteAuthority?: string): IWindowInfo { + private browserWindowToInfo(window: BrowserWindow, folderURIs: URI[] = [], remoteAuthority?: string): IWindowInfo { return { - pid: win.webContents.getOSProcessId(), - title: win.getTitle(), + pid: window.webContents.getOSProcessId(), + title: window.getTitle(), folderURIs, remoteAuthority }; diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts index 62db3f751..6d82c1224 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts @@ -576,7 +576,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe if (window && !window.isDestroyed()) { let whenWindowClosed: Promise; if (window.webContents && !window.webContents.isDestroyed()) { - whenWindowClosed = new Promise(c => window.once('closed', c)); + whenWindowClosed = new Promise(resolve => window.once('closed', resolve)); } else { whenWindowClosed = Promise.resolve(); } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index b553a0581..1d515e5bf 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -429,12 +429,14 @@ export interface IResourceNavigatorOptions { export interface SelectionKeyboardEvent extends KeyboardEvent { preserveFocus?: boolean; + pinned?: boolean; __forceEvent?: boolean; } -export function getSelectionKeyboardEvent(typeArg = 'keydown', preserveFocus?: boolean): SelectionKeyboardEvent { +export function getSelectionKeyboardEvent(typeArg = 'keydown', preserveFocus?: boolean, pinned?: boolean): SelectionKeyboardEvent { const e = new KeyboardEvent(typeArg); (e).preserveFocus = preserveFocus; + (e).pinned = pinned; (e).__forceEvent = true; return e; @@ -478,8 +480,9 @@ abstract class ResourceNavigator extends Disposable { const focus = this.widget.getFocus(); this.widget.setSelection(focus, event.browserEvent); - const preserveFocus = typeof (event.browserEvent as SelectionKeyboardEvent).preserveFocus === 'boolean' ? (event.browserEvent as SelectionKeyboardEvent).preserveFocus! : true; - const pinned = !preserveFocus; + const selectionKeyboardEvent = event.browserEvent as SelectionKeyboardEvent; + const preserveFocus = typeof selectionKeyboardEvent.preserveFocus === 'boolean' ? selectionKeyboardEvent.preserveFocus! : true; + const pinned = typeof selectionKeyboardEvent.pinned === 'boolean' ? selectionKeyboardEvent.pinned! : !preserveFocus; const sideBySide = false; this._open(this.getSelectedElement(), preserveFocus, pinned, sideBySide, event.browserEvent); @@ -490,8 +493,9 @@ abstract class ResourceNavigator extends Disposable { return; } - const preserveFocus = typeof (event.browserEvent as SelectionKeyboardEvent).preserveFocus === 'boolean' ? (event.browserEvent as SelectionKeyboardEvent).preserveFocus! : true; - const pinned = !preserveFocus; + const selectionKeyboardEvent = event.browserEvent as SelectionKeyboardEvent; + const preserveFocus = typeof selectionKeyboardEvent.preserveFocus === 'boolean' ? selectionKeyboardEvent.preserveFocus! : true; + const pinned = typeof selectionKeyboardEvent.pinned === 'boolean' ? selectionKeyboardEvent.pinned! : !preserveFocus; const sideBySide = false; this._open(this.getSelectedElement(), preserveFocus, pinned, sideBySide, event.browserEvent); @@ -826,7 +830,7 @@ function workbenchTreeDataPreamble(keyboardNavigationSettingKey); + const keyboardNavigation = options.simpleKeyboardNavigation || accessibilityOn ? 'simple' : configurationService.getValue(keyboardNavigationSettingKey); const horizontalScrolling = options.horizontalScrolling !== undefined ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); const [workbenchListOptions, disposable] = toWorkbenchListOptions(options, configurationService, keybindingService); const additionalScrollHeight = options.additionalScrollHeight; diff --git a/src/vs/platform/localizations/common/localizations.ts b/src/vs/platform/localizations/common/localizations.ts index 509c4ada8..27fa81bae 100644 --- a/src/vs/platform/localizations/common/localizations.ts +++ b/src/vs/platform/localizations/common/localizations.ts @@ -25,6 +25,8 @@ export interface ILocalizationsService { readonly onDidLanguagesChange: Event; getLanguageIds(): Promise; + + update(): Promise; } export function isValidLocalization(localization: ILocalization): boolean { diff --git a/src/vs/platform/log/node/loggerService.ts b/src/vs/platform/log/node/loggerService.ts index 7120d99d2..49fe8ad45 100644 --- a/src/vs/platform/log/node/loggerService.ts +++ b/src/vs/platform/log/node/loggerService.ts @@ -9,8 +9,8 @@ import { URI } from 'vs/base/common/uri'; import { basename, extname, dirname } from 'vs/base/common/resources'; import { Schemas } from 'vs/base/common/network'; import { FileLogService } from 'vs/platform/log/common/fileLogService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; +import { IFileService } from 'vs/platform/files/common/files'; export class LoggerService extends Disposable implements ILoggerService { @@ -20,7 +20,7 @@ export class LoggerService extends Disposable implements ILoggerService { constructor( @ILogService private logService: ILogService, - @IInstantiationService private instantiationService: IInstantiationService, + @IFileService private fileService: IFileService ) { super(); this._register(logService.onDidChangeLogLevel(level => this.loggers.forEach(logger => logger.setLevel(level)))); @@ -34,7 +34,7 @@ export class LoggerService extends Disposable implements ILoggerService { const ext = extname(resource); logger = new SpdLogService(baseName.substring(0, baseName.length - ext.length), dirname(resource).fsPath, this.logService.getLevel()); } else { - logger = this.instantiationService.createInstance(FileLogService, basename(resource), resource, this.logService.getLevel()); + logger = new FileLogService(basename(resource), resource, this.logService.getLevel(), this.fileService); } this.loggers.set(resource.toString(), logger); } diff --git a/src/vs/platform/markers/common/markerService.ts b/src/vs/platform/markers/common/markerService.ts index 537ade060..1c0c67c25 100644 --- a/src/vs/platform/markers/common/markerService.ts +++ b/src/vs/platform/markers/common/markerService.ts @@ -145,14 +145,11 @@ export class MarkerService implements IMarkerService { readonly onMarkerChanged: Event = Event.debounce(this._onMarkerChanged.event, MarkerService._debouncer, 0); private readonly _data = new DoubleResourceMap(); - private readonly _stats: MarkerStats; - - constructor() { - this._stats = new MarkerStats(this); - } + private readonly _stats = new MarkerStats(this); dispose(): void { this._stats.dispose(); + this._onMarkerChanged.dispose(); } getStatistics(): MarkerStatistics { diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 3039b01a1..32f6b799e 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -6,9 +6,8 @@ import * as nls from 'vs/nls'; import { isMacintosh, language } from 'vs/base/common/platform'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { app, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, Event, KeyboardEvent } from 'electron'; +import { app, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, KeyboardEvent } from 'electron'; import { getTitleBarStyle, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUpdateService, StateType } from 'vs/platform/update/common/update'; @@ -16,7 +15,7 @@ import product from 'vs/platform/product/common/product'; import { RunOnceScheduler } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; import { mnemonicMenuLabel } from 'vs/base/common/labels'; -import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, IWindowsCountChangedEvent, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/common/menubar'; import { URI } from 'vs/base/common/uri'; @@ -62,7 +61,7 @@ export class Menubar { private keybindings: { [commandId: string]: IMenubarKeybinding }; - private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow | undefined, event: Event) => void } = Object.create(null); + private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow | undefined, event: KeyboardEvent) => void } = Object.create(null); constructor( @IUpdateService private readonly updateService: IUpdateService, @@ -608,45 +607,18 @@ export class Menubar { } } - private static _menuItemIsTriggeredViaKeybinding(event: KeyboardEvent, userSettingsLabel: string): boolean { - // The event coming in from Electron will inform us only about the modifier keys pressed. - // The strategy here is to check if the modifier keys match those of the keybinding, - // since it is highly unlikely to use modifier keys when clicking with the mouse - if (!userSettingsLabel) { - // There is no keybinding - return false; - } - - let ctrlRequired = /ctrl/.test(userSettingsLabel); - let shiftRequired = /shift/.test(userSettingsLabel); - let altRequired = /alt/.test(userSettingsLabel); - let metaRequired = /cmd/.test(userSettingsLabel) || /super/.test(userSettingsLabel); - - if (!ctrlRequired && !shiftRequired && !altRequired && !metaRequired) { - // This keybinding does not use any modifier keys, so we cannot use this heuristic - return false; - } - - return ( - ctrlRequired === event.ctrlKey - && shiftRequired === event.shiftKey - && altRequired === event.altKey - && metaRequired === event.metaKey - ); - } - private createMenuItem(label: string, commandId: string | string[], enabled?: boolean, checked?: boolean): MenuItem; private createMenuItem(label: string, click: () => void, enabled?: boolean, checked?: boolean): MenuItem; private createMenuItem(arg1: string, arg2: any, arg3?: boolean, arg4?: boolean): MenuItem { const label = this.mnemonicLabel(arg1); - const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: MenuItem & IMenuItemWithKeybinding, win: BrowserWindow, event: Event) => { + const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: MenuItem & IMenuItemWithKeybinding, win: BrowserWindow, event: KeyboardEvent) => { const userSettingsLabel = menuItem ? menuItem.userSettingsLabel : null; let commandId = arg2; if (Array.isArray(arg2)) { commandId = this.isOptionClick(event) ? arg2[1] : arg2[0]; // support alternative action if we got multiple action Ids and the option key was pressed while invoking } - if (userSettingsLabel && Menubar._menuItemIsTriggeredViaKeybinding(event, userSettingsLabel)) { + if (userSettingsLabel && event.triggeredByAccelerator) { this.runActionInRenderer({ type: 'keybinding', userSettingsLabel }); } else { this.runActionInRenderer({ type: 'commandId', commandId }); @@ -706,8 +678,8 @@ export class Menubar { return new MenuItem(this.withKeybinding(commandId, options)); } - private makeContextAwareClickHandler(click: () => void, contextSpecificHandlers: IMenuItemClickHandler): () => void { - return () => { + private makeContextAwareClickHandler(click: (menuItem: MenuItem, win: BrowserWindow, event: KeyboardEvent) => void, contextSpecificHandlers: IMenuItemClickHandler): (menuItem: MenuItem, win: BrowserWindow | undefined, event: KeyboardEvent) => void { + return (menuItem: MenuItem, win: BrowserWindow | undefined, event: KeyboardEvent) => { // No Active Window const activeWindow = BrowserWindow.getFocusedWindow(); @@ -722,7 +694,7 @@ export class Menubar { } // Finally execute command in Window - click(); + click(menuItem, win || activeWindow, event); }; } diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 60baf2da4..2c8414ba8 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -49,7 +49,7 @@ export interface ICommonNativeHostService { readonly onDidChangeColorScheme: Event; - readonly onDidChangePassword: Event; + readonly onDidChangePassword: Event<{ service: string, account: string }>; // Window getWindows(): Promise; @@ -137,6 +137,7 @@ export interface ICommonNativeHostService { // Development openDevTools(options?: OpenDevToolsOptions): Promise; toggleDevTools(): Promise; + toggleSharedProcessWindow(): Promise; sendInputEvent(event: MouseInputEvent): Promise; // Connectivity diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index e24efa83c..50c4460ad 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -4,18 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor, nativeTheme } from 'electron'; -import { OpenContext } from 'vs/platform/windows/node/window'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; -import { isMacintosh, isWindows, isLinux } from 'vs/base/common/platform'; +import { isMacintosh, isWindows, isLinux, isLinuxSnap } from 'vs/base/common/platform'; import { ICommonNativeHostService, IOSProperties, IOSStatistics } from 'vs/platform/native/common/native'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { AddFirstParameterToFunctions } from 'vs/base/common/types'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { dirExists } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -28,6 +27,7 @@ import { dirname, join } from 'vs/base/common/path'; import product from 'vs/platform/product/common/product'; import { memoize } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; +import { ISharedProcess } from 'vs/platform/sharedProcess/node/sharedProcess'; export interface INativeHostMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } @@ -43,6 +43,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain declare readonly _serviceBrand: undefined; constructor( + private sharedProcess: ISharedProcess, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IDialogMainService private readonly dialogMainService: IDialogMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @@ -91,7 +92,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain private readonly _onDidChangeColorScheme = this._register(new Emitter()); readonly onDidChangeColorScheme = this._onDidChangeColorScheme.event; - private readonly _onDidChangePassword = this._register(new Emitter()); + private readonly _onDidChangePassword = this._register(new Emitter<{ account: string, service: string }>()); readonly onDidChangePassword = this._onDidChangePassword.event; //#endregion @@ -104,7 +105,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return windows.map(window => ({ id: window.id, workspace: window.openedWorkspace, - folderUri: window.openedFolderUri, title: window.win.getTitle(), filename: window.getRepresentedFilename(), dirty: window.isDocumentEdited() @@ -334,8 +334,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } async openExternal(windowId: number | undefined, url: string): Promise { - if (isLinux && process.env.SNAP && process.env.SNAP_REVISION) { - NativeHostMainService._safeSnapOpenExternal(url); + if (isLinuxSnap) { + this.safeSnapOpenExternal(url); } else { shell.openExternal(url); } @@ -343,7 +343,9 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return true; } - private static _safeSnapOpenExternal(url: string): void { + private safeSnapOpenExternal(url: string): void { + + // Remove some environment variables before opening to avoid issues... const gdkPixbufModuleFile = process.env['GDK_PIXBUF_MODULE_FILE']; const gdkPixbufModuleDir = process.env['GDK_PIXBUF_MODULEDIR']; delete process.env['GDK_PIXBUF_MODULE_FILE']; @@ -351,6 +353,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain shell.openExternal(url); + // ...but restore them after process.env['GDK_PIXBUF_MODULE_FILE'] = gdkPixbufModuleFile; process.env['GDK_PIXBUF_MODULEDIR'] = gdkPixbufModuleDir; } @@ -626,6 +629,10 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } } + async toggleSharedProcessWindow(): Promise { + return this.sharedProcess.toggle(); + } + //#endregion //#region Registry (windows) @@ -703,7 +710,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain await keytar.setPassword(service, account, password); } - this._onDidChangePassword.fire(); + this._onDidChangePassword.fire({ service, account }); } async deletePassword(windowId: number | undefined, service: string, account: string): Promise { @@ -711,7 +718,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain const didDelete = await keytar.deletePassword(service, account); if (didDelete) { - this._onDidChangePassword.fire(); + this._onDidChangePassword.fire({ service, account }); } return didDelete; diff --git a/src/vs/platform/opener/browser/link.ts b/src/vs/platform/opener/browser/link.ts index 7b6863cd0..ca887940f 100644 --- a/src/vs/platform/opener/browser/link.ts +++ b/src/vs/platform/opener/browser/link.ts @@ -20,11 +20,13 @@ export interface ILinkDescriptor { export interface ILinkStyles { readonly textLinkForeground?: Color; + readonly disabled?: boolean; } export class Link extends Disposable { readonly el: HTMLAnchorElement; + private disabled: boolean; private styles: ILinkStyles = { textLinkForeground: Color.fromHex('#006AB1') }; @@ -50,9 +52,12 @@ export class Link extends Disposable { this._register(onOpen(e => { EventHelper.stop(e, true); - openerService.open(link.href); + if (!this.disabled) { + openerService.open(link.href); + } })); + this.disabled = false; this.applyStyles(); } @@ -62,6 +67,26 @@ export class Link extends Disposable { } private applyStyles(): void { - this.el.style.color = this.styles.textLinkForeground?.toString() || ''; + const color = this.styles.textLinkForeground?.toString(); + if (color) { + this.el.style.color = color; + } + if (typeof this.styles.disabled === 'boolean' && this.styles.disabled !== this.disabled) { + if (this.styles.disabled) { + this.el.setAttribute('aria-disabled', 'true'); + this.el.tabIndex = -1; + this.el.style.pointerEvents = 'none'; + this.el.style.opacity = '0.4'; + this.el.style.cursor = 'default'; + this.disabled = true; + } else { + this.el.setAttribute('aria-disabled', 'false'); + this.el.tabIndex = 0; + this.el.style.pointerEvents = 'auto'; + this.el.style.opacity = '1'; + this.el.style.cursor = 'pointer'; + this.disabled = false; + } + } } } diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index d82651f93..ff6740cf6 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { equalsIgnoreCase, startsWithIgnoreCase } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const IOpenerService = createDecorator('openerService'); -type OpenInternalOptions = { +export type OpenInternalOptions = { /** * Signals that the intent is to open an editor to the side @@ -31,7 +32,11 @@ type OpenInternalOptions = { readonly fromUserGesture?: boolean; }; -type OpenExternalOptions = { readonly openExternal?: boolean; readonly allowTunneling?: boolean }; +export type OpenExternalOptions = { + readonly openExternal?: boolean; + readonly allowTunneling?: boolean; + readonly allowContributedOpeners?: boolean | string; +}; export type OpenOptions = OpenInternalOptions & OpenExternalOptions; @@ -46,7 +51,8 @@ export interface IOpener { } export interface IExternalOpener { - openExternal(href: string): Promise; + openExternal(href: string, ctx: { sourceUri: URI, preferredOpenerId?: string }, token: CancellationToken): Promise; + dispose?(): void; } export interface IValidator { @@ -81,7 +87,12 @@ export interface IOpenerService { * Sets the handler for opening externally. If not provided, * a default handler will be used. */ - setExternalOpener(opener: IExternalOpener): void; + setDefaultExternalOpener(opener: IExternalOpener): void; + + /** + * Registers a new opener external resources openers. + */ + registerExternalOpener(opener: IExternalOpener): IDisposable; /** * Opens a resource, like a webaddress, a document uri, or executes command. @@ -97,15 +108,16 @@ export interface IOpenerService { resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise; } -export const NullOpenerService: IOpenerService = Object.freeze({ +export const NullOpenerService = Object.freeze({ _serviceBrand: undefined, registerOpener() { return Disposable.None; }, registerValidator() { return Disposable.None; }, registerExternalUriResolver() { return Disposable.None; }, - setExternalOpener() { }, + setDefaultExternalOpener() { }, + registerExternalOpener() { return Disposable.None; }, async open() { return false; }, async resolveExternalUri(uri: URI) { return { resolved: uri, dispose() { } }; }, -}); +} as IOpenerService); export function matchesScheme(target: URI | string, scheme: string) { if (URI.isUri(target)) { diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 56fbc68ad..537db557d 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -20,7 +20,7 @@ if (isWeb || typeof require === 'undefined' || typeof require.__$__nodeRequire ! // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.52.0-dev', + version: '1.53.0-dev', nameShort: isWeb ? 'Code Web - OSS Dev' : 'Code - OSS Dev', nameLong: isWeb ? 'Code Web - OSS Dev' : 'Code - OSS Dev', applicationName: 'code-oss', diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts index 74a8c744a..92b8c1a6d 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -129,6 +129,8 @@ export interface IProductConfiguration { readonly linkProtectionTrustedDomains?: readonly string[]; readonly 'configurationSync.store'?: ConfigurationSyncStore; + + readonly darwinUniversalAssetId?: string; } export type ImportantExtensionTip = { name: string; languages?: string[]; pattern?: string; isExtensionPack?: boolean }; diff --git a/src/vs/platform/remote/browser/browserSocketFactory.ts b/src/vs/platform/remote/browser/browserSocketFactory.ts index 3715cbb8e..2f343e841 100644 --- a/src/vs/platform/remote/browser/browserSocketFactory.ts +++ b/src/vs/platform/remote/browser/browserSocketFactory.ts @@ -208,7 +208,7 @@ export class BrowserSocketFactory implements ISocketFactory { } connect(host: string, port: number, query: string, callback: IConnectCallback): void { - const socket = this._webSocketFactory.create(`ws://${host}:${port}/?${query}&skipWebSocketFrames=false`); + const socket = this._webSocketFactory.create(`ws://${/:/.test(host) ? `[${host}]` : host}:${port}/?${query}&skipWebSocketFrames=false`); const errorListener = socket.onError((err) => callback(err, undefined)); socket.onOpen(() => { errorListener.dispose(); diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts index fdd5890c6..82791249f 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -8,7 +8,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { Disposable } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; import { ISignService } from 'vs/platform/sign/common/sign'; @@ -86,96 +86,124 @@ export interface ISocketFactory { connect(host: string, port: number, query: string, callback: IConnectCallback): void; } -async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }> { - const logPrefix = connectLogPrefix(options, connectionType); - const { protocol, ownsProtocol } = await new Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }>((c, e) => { - options.logService.trace(`${logPrefix} 1/6. invoking socketFactory.connect().`); - options.socketFactory.connect( - options.host, - options.port, - `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, - (err: any, socket: ISocket | undefined) => { - if (err || !socket) { - options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`); - options.logService.error(err); - e(err); - return; - } +async function readOneControlMessage(protocol: PersistentProtocol): Promise { + const raw = await Event.toPromise(protocol.onControlMessage); + const msg = JSON.parse(raw.toString()); + const error = getErrorFromMessage(msg); + if (error) { + throw error; + } + return msg; +} - options.logService.trace(`${logPrefix} 2/6. socketFactory.connect() was successful.`); - if (options.reconnectionProtocol) { - options.reconnectionProtocol.beginAcceptReconnection(socket, null); - c({ protocol: options.reconnectionProtocol, ownsProtocol: false }); - } else { - c({ protocol: new PersistentProtocol(socket, null), ownsProtocol: true }); - } +function waitWithTimeout(promise: Promise, timeout: number): Promise { + return new Promise((resolve, reject) => { + const timeoutToken = setTimeout(() => { + const error: any = new Error('Timeout'); + error.code = 'ETIMEDOUT'; + error.syscall = 'connect'; + reject(error); + }, timeout); + + promise.then( + (result) => { + clearTimeout(timeoutToken); + resolve(result); + }, + (error) => { + clearTimeout(timeoutToken); + reject(error); } ); }); +} - return new Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }>((c, e) => { +function createSocket(socketFactory: ISocketFactory, host: string, port: number, query: string): Promise { + return new Promise((resolve, reject) => { + socketFactory.connect(host, port, query, (err: any, socket: ISocket | undefined) => { + if (err || !socket) { + return reject(err); + } + resolve(socket); + }); + }); +} - const errorTimeoutToken = setTimeout(() => { - const error: any = new Error('handshake timeout'); - error.code = 'ETIMEDOUT'; - error.syscall = 'connect'; +async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }> { + const logPrefix = connectLogPrefix(options, connectionType); + + options.logService.trace(`${logPrefix} 1/6. invoking socketFactory.connect().`); + + let socket: ISocket; + try { + socket = await createSocket(options.socketFactory, options.host, options.port, `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`); + } catch (error) { + options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`); + options.logService.error(error); + throw error; + } + + options.logService.trace(`${logPrefix} 2/6. socketFactory.connect() was successful.`); + + let protocol: PersistentProtocol; + let ownsProtocol: boolean; + if (options.reconnectionProtocol) { + options.reconnectionProtocol.beginAcceptReconnection(socket, null); + protocol = options.reconnectionProtocol; + ownsProtocol = false; + } else { + protocol = new PersistentProtocol(socket, null); + ownsProtocol = true; + } + + options.logService.trace(`${logPrefix} 3/6. sending AuthRequest control message.`); + const authRequest: AuthRequest = { + type: 'auth', + auth: options.connectionToken || '00000000000000000000' + }; + protocol.sendControl(VSBuffer.fromString(JSON.stringify(authRequest))); + + try { + const msg = await waitWithTimeout(readOneControlMessage(protocol), 10000); + + if (msg.type !== 'sign' || typeof msg.data !== 'string') { + const error: any = new Error('Unexpected handshake message'); + error.code = 'VSCODE_CONNECTION_ERROR'; + throw error; + } + + options.logService.trace(`${logPrefix} 4/6. received SignRequest control message.`); + + const signed = await options.signService.sign(msg.data); + const connTypeRequest: ConnectionTypeRequest = { + type: 'connectionType', + commit: options.commit, + signedData: signed, + desiredConnectionType: connectionType + }; + if (args) { + connTypeRequest.args = args; + } + + options.logService.trace(`${logPrefix} 5/6. sending ConnectionTypeRequest control message.`); + protocol.sendControl(VSBuffer.fromString(JSON.stringify(connTypeRequest))); + + return { protocol, ownsProtocol }; + + } catch (error) { + if (error && error.code === 'ETIMEDOUT') { options.logService.error(`${logPrefix} the handshake took longer than 10 seconds. Error:`); options.logService.error(error); - if (ownsProtocol) { - safeDisposeProtocolAndSocket(protocol); - } - e(error); - }, 10000); - - const messageRegistration = protocol.onControlMessage(async raw => { - const msg = JSON.parse(raw.toString()); - // Stop listening for further events - messageRegistration.dispose(); - - const error = getErrorFromMessage(msg); - if (error) { - options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`); - options.logService.error(error); - if (ownsProtocol) { - safeDisposeProtocolAndSocket(protocol); - } - return e(error); - } - - if (msg.type === 'sign') { - options.logService.trace(`${logPrefix} 4/6. received SignRequest control message.`); - const signed = await options.signService.sign(msg.data); - const connTypeRequest: ConnectionTypeRequest = { - type: 'connectionType', - commit: options.commit, - signedData: signed, - desiredConnectionType: connectionType - }; - if (args) { - connTypeRequest.args = args; - } - options.logService.trace(`${logPrefix} 5/6. sending ConnectionTypeRequest control message.`); - protocol.sendControl(VSBuffer.fromString(JSON.stringify(connTypeRequest))); - clearTimeout(errorTimeoutToken); - c({ protocol, ownsProtocol }); - } else { - const error = new Error('handshake error'); - options.logService.error(`${logPrefix} received unexpected control message. Error:`); - options.logService.error(error); - if (ownsProtocol) { - safeDisposeProtocolAndSocket(protocol); - } - e(error); - } - }); - - options.logService.trace(`${logPrefix} 3/6. sending AuthRequest control message.`); - const authRequest: AuthRequest = { - type: 'auth', - auth: options.connectionToken || '00000000000000000000' - }; - protocol.sendControl(VSBuffer.fromString(JSON.stringify(authRequest))); - }); + } + if (error && error.code === 'VSCODE_CONNECTION_ERROR') { + options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`); + options.logService.error(error); + } + if (ownsProtocol) { + safeDisposeProtocolAndSocket(protocol); + } + throw error; + } } interface IManagementConnectionResult { @@ -287,7 +315,7 @@ export async function connectRemoteAgentManagement(options: IConnectionOptions, } catch (err) { options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`); options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(0, 0, RemoteAuthorityResolverError.isHandled(err)); throw err; } } @@ -301,7 +329,7 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption } catch (err) { options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`); options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(0, 0, RemoteAuthorityResolverError.isHandled(err)); throw err; } } @@ -333,10 +361,16 @@ export const enum PersistentConnectionEventType { } export class ConnectionLostEvent { public readonly type = PersistentConnectionEventType.ConnectionLost; + constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number + ) { } } export class ReconnectionWaitEvent { public readonly type = PersistentConnectionEventType.ReconnectionWait; constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number, public readonly durationSeconds: number, private readonly cancellableTimer: CancelablePromise ) { } @@ -347,22 +381,44 @@ export class ReconnectionWaitEvent { } export class ReconnectionRunningEvent { public readonly type = PersistentConnectionEventType.ReconnectionRunning; + constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number, + public readonly attempt: number + ) { } } export class ConnectionGainEvent { public readonly type = PersistentConnectionEventType.ConnectionGain; + constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number, + public readonly attempt: number + ) { } } export class ReconnectionPermanentFailureEvent { public readonly type = PersistentConnectionEventType.ReconnectionPermanentFailure; + constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number, + public readonly attempt: number, + public readonly handled: boolean + ) { } } export type PersistentConnectionEvent = ConnectionGainEvent | ConnectionLostEvent | ReconnectionWaitEvent | ReconnectionRunningEvent | ReconnectionPermanentFailureEvent; abstract class PersistentConnection extends Disposable { - public static triggerPermanentFailure(): void { + public static triggerPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void { this._permanentFailure = true; - this._instances.forEach(instance => instance._gotoPermanentFailure()); + this._permanentFailureMillisSinceLastIncomingData = millisSinceLastIncomingData; + this._permanentFailureAttempt = attempt; + this._permanentFailureHandled = handled; + this._instances.forEach(instance => instance._gotoPermanentFailure(this._permanentFailureMillisSinceLastIncomingData, this._permanentFailureAttempt, this._permanentFailureHandled)); } private static _permanentFailure: boolean = false; + private static _permanentFailureMillisSinceLastIncomingData: number = 0; + private static _permanentFailureAttempt: number = 0; + private static _permanentFailureHandled: boolean = false; private static _instances: PersistentConnection[] = []; private readonly _onDidStateChange = this._register(new Emitter()); @@ -381,7 +437,7 @@ abstract class PersistentConnection extends Disposable { this.protocol = protocol; this._isReconnecting = false; - this._onDidStateChange.fire(new ConnectionGainEvent()); + this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, 0, 0)); this._register(protocol.onSocketClose(() => this._beginReconnecting())); this._register(protocol.onSocketTimeout(() => this._beginReconnecting())); @@ -389,7 +445,7 @@ abstract class PersistentConnection extends Disposable { PersistentConnection._instances.push(this); if (PersistentConnection._permanentFailure) { - this._gotoPermanentFailure(); + this._gotoPermanentFailure(PersistentConnection._permanentFailureMillisSinceLastIncomingData, PersistentConnection._permanentFailureAttempt, PersistentConnection._permanentFailureHandled); } } @@ -413,21 +469,23 @@ abstract class PersistentConnection extends Disposable { } const logPrefix = commonLogPrefix(this._connectionType, this.reconnectionToken, true); this._options.logService.info(`${logPrefix} starting reconnecting loop. You can get more information with the trace log level.`); - this._onDidStateChange.fire(new ConnectionLostEvent()); - const TIMES = [5, 5, 10, 10, 10, 10, 10, 30]; + this._onDidStateChange.fire(new ConnectionLostEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData())); + const TIMES = [0, 5, 5, 10, 10, 10, 10, 10, 30]; const disconnectStartTime = Date.now(); let attempt = -1; do { attempt++; const waitTime = (attempt < TIMES.length ? TIMES[attempt] : TIMES[TIMES.length - 1]); try { - const sleepPromise = sleep(waitTime); - this._onDidStateChange.fire(new ReconnectionWaitEvent(waitTime, sleepPromise)); + if (waitTime > 0) { + const sleepPromise = sleep(waitTime); + this._onDidStateChange.fire(new ReconnectionWaitEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), waitTime, sleepPromise)); - this._options.logService.info(`${logPrefix} waiting for ${waitTime} seconds before reconnecting...`); - try { - await sleepPromise; - } catch { } // User canceled timer + this._options.logService.info(`${logPrefix} waiting for ${waitTime} seconds before reconnecting...`); + try { + await sleepPromise; + } catch { } // User canceled timer + } if (PersistentConnection._permanentFailure) { this._options.logService.error(`${logPrefix} permanent failure occurred while running the reconnecting loop.`); @@ -435,26 +493,26 @@ abstract class PersistentConnection extends Disposable { } // connection was lost, let's try to re-establish it - this._onDidStateChange.fire(new ReconnectionRunningEvent()); + this._onDidStateChange.fire(new ReconnectionRunningEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1)); this._options.logService.info(`${logPrefix} resolving connection...`); const simpleOptions = await resolveConnectionOptions(this._options, this.reconnectionToken, this.protocol); this._options.logService.info(`${logPrefix} connecting to ${simpleOptions.host}:${simpleOptions.port}...`); await connectWithTimeLimit(simpleOptions.logService, this._reconnect(simpleOptions), RECONNECT_TIMEOUT); this._options.logService.info(`${logPrefix} reconnected!`); - this._onDidStateChange.fire(new ConnectionGainEvent()); + this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1)); break; } catch (err) { if (err.code === 'VSCODE_CONNECTION_ERROR') { this._options.logService.error(`${logPrefix} A permanent error occurred in the reconnecting loop! Will give up now! Error:`); this._options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false); break; } if (Date.now() - disconnectStartTime > ProtocolConstants.ReconnectionGraceTime) { this._options.logService.error(`${logPrefix} An error occurred while reconnecting, but it will be treated as a permanent error because the reconnection grace time has expired! Will give up now! Error:`); this._options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false); break; } if (RemoteAuthorityResolverError.isTemporarilyNotAvailable(err)) { @@ -475,16 +533,22 @@ abstract class PersistentConnection extends Disposable { // try again! continue; } + if (err instanceof RemoteAuthorityResolverError) { + this._options.logService.error(`${logPrefix} A RemoteAuthorityResolverError occurred while trying to reconnect. Will give up now! Error:`); + this._options.logService.error(err); + PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, RemoteAuthorityResolverError.isHandled(err)); + break; + } this._options.logService.error(`${logPrefix} An unknown error occurred while trying to reconnect, since this is an unknown case, it will be treated as a permanent error! Will give up now! Error:`); this._options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false); break; } } while (!PersistentConnection._permanentFailure); } - private _gotoPermanentFailure(): void { - this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent()); + private _gotoPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void { + this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent(this.reconnectionToken, millisSinceLastIncomingData, attempt, handled)); safeDisposeProtocolAndSocket(this.protocol); } diff --git a/src/vs/platform/remote/common/remoteAgentEnvironment.ts b/src/vs/platform/remote/common/remoteAgentEnvironment.ts index 3052141ff..fd2752809 100644 --- a/src/vs/platform/remote/common/remoteAgentEnvironment.ts +++ b/src/vs/platform/remote/common/remoteAgentEnvironment.ts @@ -5,6 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { OperatingSystem } from 'vs/base/common/platform'; +import * as performance from 'vs/base/common/performance'; export interface IRemoteAgentEnvironment { pid: number; @@ -18,6 +19,7 @@ export interface IRemoteAgentEnvironment { workspaceStorageHome: URI; userHome: URI; os: OperatingSystem; + marks: performance.PerformanceMark[]; } export interface RemoteAgentConnectionContext { diff --git a/src/vs/platform/remote/common/remoteHosts.ts b/src/vs/platform/remote/common/remoteHosts.ts index 1f3ab36f3..86f2c4f26 100644 --- a/src/vs/platform/remote/common/remoteHosts.ts +++ b/src/vs/platform/remote/common/remoteHosts.ts @@ -19,7 +19,7 @@ export function getRemoteName(authority: string | undefined): string | undefined } const pos = authority.indexOf('+'); if (pos < 0) { - // funky? bad authority? + // e.g. localhost:8000 return authority; } return authority.substr(0, pos); diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index 980c3203a..b4009323b 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -5,6 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { isWindows, OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -17,36 +18,64 @@ export interface RemoteTunnel { readonly tunnelRemoteHost: string; readonly tunnelLocalPort?: number; readonly localAddress: string; - dispose(silent?: boolean): void; + readonly public: boolean; + dispose(silent?: boolean): Promise; } export interface TunnelOptions { - remoteAddress: { port: number, host: string }; + remoteAddress: { port: number, host: string; }; localAddressPort?: number; label?: string; + public?: boolean; } export interface TunnelCreationOptions { elevationRequired?: boolean; } +export interface TunnelProviderFeatures { + elevation: boolean; + public: boolean; +} + export interface ITunnelProvider { - forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined; + forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined; +} + +export interface ITunnel { + remoteAddress: { port: number, host: string }; + + /** + * The complete local address(ex. localhost:1234) + */ + localAddress: string; + + public?: boolean; + + /** + * Implementers of Tunnel should fire onDidDispose when dispose is called. + */ + onDidDispose: Event; + + dispose(): Promise | void; } export interface ITunnelService { readonly _serviceBrand: undefined; readonly tunnels: Promise; + readonly canMakePublic: boolean; readonly onTunnelOpened: Event; - readonly onTunnelClosed: Event<{ host: string, port: number }>; + readonly onTunnelClosed: Event<{ host: string, port: number; }>; + readonly canElevate: boolean; - openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise | undefined; + canTunnel(uri: URI): boolean; + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, isPublic?: boolean): Promise | undefined; closeTunnel(remoteHost: string, remotePort: number): Promise; - setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable; + setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable; } -export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number } | undefined { +export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number; } | undefined { if (uri.scheme !== 'http' && uri.scheme !== 'https') { return undefined; } @@ -74,51 +103,88 @@ function getOtherLocalhost(host: string): string | undefined { return (host === 'localhost') ? '127.0.0.1' : ((host === '127.0.0.1') ? 'localhost' : undefined); } +export function isPortPrivileged(port: number, os?: OperatingSystem): boolean { + if (os) { + return os !== OperatingSystem.Windows && (port < 1024); + } else { + return !isWindows && (port < 1024); + } +} + export abstract class AbstractTunnelService implements ITunnelService { declare readonly _serviceBrand: undefined; private _onTunnelOpened: Emitter = new Emitter(); public onTunnelOpened: Event = this._onTunnelOpened.event; - private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter(); - public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event; - protected readonly _tunnels = new Map }>>(); + private _onTunnelClosed: Emitter<{ host: string, port: number; }> = new Emitter(); + public onTunnelClosed: Event<{ host: string, port: number; }> = this._onTunnelClosed.event; + protected readonly _tunnels = new Map; }>>(); protected _tunnelProvider: ITunnelProvider | undefined; + protected _canElevate: boolean = false; + private _canMakePublic: boolean = false; public constructor( @ILogService protected readonly logService: ILogService ) { } - setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { + setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable { + this._tunnelProvider = provider; if (!provider) { + // clear features + this._canElevate = false; + this._canMakePublic = false; return { dispose: () => { } }; } - this._tunnelProvider = provider; + this._canElevate = features.elevation; + this._canMakePublic = features.public; return { dispose: () => { this._tunnelProvider = undefined; + this._canElevate = false; + this._canMakePublic = false; } }; } - public get tunnels(): Promise { - const promises: Promise[] = []; - Array.from(this._tunnels.values()).forEach(portMap => Array.from(portMap.values()).forEach(x => promises.push(x.value))); - return Promise.all(promises); + public get canElevate(): boolean { + return this._canElevate; } - dispose(): void { + public get canMakePublic() { + return this._canMakePublic; + } + + public get tunnels(): Promise { + return new Promise(async (resolve) => { + const tunnels: RemoteTunnel[] = []; + const tunnelArray = Array.from(this._tunnels.values()); + for (let portMap of tunnelArray) { + const portArray = Array.from(portMap.values()); + for (let x of portArray) { + const tunnelValue = await x.value; + if (tunnelValue) { + tunnels.push(tunnelValue); + } + } + } + resolve(tunnels); + }); + } + + async dispose(): Promise { for (const portMap of this._tunnels.values()) { for (const { value } of portMap.values()) { - value.then(tunnel => tunnel.dispose()); + await value.then(tunnel => tunnel?.dispose()); } portMap.clear(); } this._tunnels.clear(); } - openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort: number): Promise | undefined { + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false, isPublic: boolean = false): Promise | undefined { + this.logService.trace(`openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`); if (!addressProvider) { return undefined; } @@ -127,12 +193,19 @@ export abstract class AbstractTunnelService implements ITunnelService { remoteHost = 'localhost'; } - const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort); + const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic); if (!resolvedTunnel) { + this.logService.trace(`Tunnel was not created.`); return resolvedTunnel; } return resolvedTunnel.then(tunnel => { + if (!tunnel) { + this.logService.trace('New tunnel is undefined.'); + this.removeEmptyTunnelFromMap(remoteHost!, remotePort); + return undefined; + } + this.logService.trace('New tunnel established.'); const newTunnel = this.makeTunnel(tunnel); if (tunnel.tunnelRemoteHost !== remoteHost || tunnel.tunnelRemotePort !== remotePort) { this.logService.warn('Created tunnel does not match requirements of requested tunnel. Host or port mismatch.'); @@ -148,24 +221,28 @@ export abstract class AbstractTunnelService implements ITunnelService { tunnelRemoteHost: tunnel.tunnelRemoteHost, tunnelLocalPort: tunnel.tunnelLocalPort, localAddress: tunnel.localAddress, - dispose: () => { + public: tunnel.public, + dispose: async () => { const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost); if (existingHost) { const existing = existingHost.get(tunnel.tunnelRemotePort); if (existing) { existing.refcount--; - this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing); + await this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing); } } } }; } - private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise }): Promise { + private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise }): Promise { if (tunnel.refcount <= 0) { - const disposePromise: Promise = tunnel.value.then(tunnel => { - tunnel.dispose(true); - this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); + this.logService.trace(`Tunnel is being disposed ${remoteHost}:${remotePort}.`); + const disposePromise: Promise = tunnel.value.then(async (tunnel) => { + if (tunnel) { + await tunnel.dispose(true); + this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); + } }); if (this._tunnels.has(remoteHost)) { this._tunnels.get(remoteHost)!.delete(remotePort); @@ -183,16 +260,30 @@ export abstract class AbstractTunnelService implements ITunnelService { } } - protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise) { + protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise) { if (!this._tunnels.has(remoteHost)) { this._tunnels.set(remoteHost, new Map()); } this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel }); } - protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise } | undefined { + private async removeEmptyTunnelFromMap(remoteHost: string, remotePort: number) { + const hostMap = this._tunnels.get(remoteHost); + if (hostMap) { + const tunnel = hostMap.get(remotePort); + const tunnelResult = await tunnel; + if (!tunnelResult) { + hostMap.delete(remotePort); + } + if (hostMap.size === 0) { + this._tunnels.delete(remoteHost); + } + } + } + + protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise } | undefined { const otherLocalhost = getOtherLocalhost(remoteHost); - let portMap: Map }> | undefined; + let portMap: Map }> | undefined; if (otherLocalhost) { const firstMap = this._tunnels.get(remoteHost); const secondMap = this._tunnels.get(otherLocalhost); @@ -207,31 +298,25 @@ export abstract class AbstractTunnelService implements ITunnelService { return portMap ? portMap.get(remotePort) : undefined; } - protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined; + canTunnel(uri: URI): boolean { + return !!extractLocalHostUriMetaDataForPortMapping(uri); + } - protected isPortPrivileged(port: number): boolean { - return port < 1024; + protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise | undefined; + + protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise | undefined { + this.logService.trace(`Creating tunnel with provider ${remoteHost}:${remotePort} on local port ${localPort}.`); + + const preferredLocalPort = localPort === undefined ? remotePort : localPort; + const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false }; + const tunnelOptions: TunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort, public: isPublic }; + const tunnel = tunnelProvider.forwardPort(tunnelOptions, creationInfo); + this.logService.trace('Tunnel created by provider.'); + if (tunnel) { + this.addTunnelToMap(remoteHost, remotePort, tunnel); + } + return tunnel; } } -export class TunnelService extends AbstractTunnelService { - protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise | undefined { - const existing = this.getTunnelFromMap(remoteHost, remotePort); - if (existing) { - ++existing.refcount; - return existing.value; - } - if (this._tunnelProvider) { - const preferredLocalPort = localPort === undefined ? remotePort : localPort; - const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort }; - const creationInfo = { elevationRequired: this.isPortPrivileged(preferredLocalPort) }; - const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo); - if (tunnel) { - this.addTunnelToMap(remoteHost, remotePort, tunnel); - } - return tunnel; - } - return undefined; - } -} diff --git a/src/vs/platform/remote/node/nodeSocketFactory.ts b/src/vs/platform/remote/node/nodeSocketFactory.ts index 44d689961..0a95e23eb 100644 --- a/src/vs/platform/remote/node/nodeSocketFactory.ts +++ b/src/vs/platform/remote/node/nodeSocketFactory.ts @@ -22,7 +22,7 @@ export const nodeSocketFactory = new class implements ISocketFactory { const nonce = buffer.toString('base64'); let headers = [ - `GET ws://${host}:${port}/?${query}&skipWebSocketFrames=true HTTP/1.1`, + `GET ws://${/:/.test(host) ? `[${host}]` : host}:${port}/?${query}&skipWebSocketFrames=true HTTP/1.1`, `Connection: Upgrade`, `Upgrade: websocket`, `Sec-WebSocket-Key: ${nonce}` diff --git a/src/vs/platform/remote/node/tunnelService.ts b/src/vs/platform/remote/node/tunnelService.ts index 19ec1efd6..80ef45f22 100644 --- a/src/vs/platform/remote/node/tunnelService.ts +++ b/src/vs/platform/remote/node/tunnelService.ts @@ -10,7 +10,7 @@ import { findFreePortFaster } from 'vs/base/node/ports'; import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; -import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; +import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; import { ISignService } from 'vs/platform/sign/common/sign'; @@ -26,6 +26,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { public tunnelLocalPort!: number; public tunnelRemoteHost: string; public localAddress!: string; + public readonly public = false; private readonly _options: IConnectionOptions; private readonly _server: net.Server; @@ -57,7 +58,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { this.tunnelRemoteHost = tunnelRemoteHost; } - public dispose(): void { + public async dispose(): Promise { super.dispose(); this._server.removeListener('listening', this._listeningListener); this._server.removeListener('connection', this._connectionListener); @@ -129,8 +130,9 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { } } -export class TunnelService extends AbstractTunnelService { +export class BaseTunnelService extends AbstractTunnelService { public constructor( + private readonly socketFactory: ISocketFactory, @ILogService logService: ILogService, @ISignService private readonly signService: ISignService, @IProductService private readonly productService: IProductService @@ -138,7 +140,7 @@ export class TunnelService extends AbstractTunnelService { super(logService); } - protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined { + protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; @@ -146,18 +148,12 @@ export class TunnelService extends AbstractTunnelService { } if (this._tunnelProvider) { - const preferredLocalPort = localPort === undefined ? remotePort : localPort; - const creationInfo = { elevationRequired: this.isPortPrivileged(preferredLocalPort) }; - const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort }; - const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo); - if (tunnel) { - this.addTunnelToMap(remoteHost, remotePort, tunnel); - } - return tunnel; + return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic); } else { + this.logService.trace(`Creating tunnel without provider ${remoteHost}:${remotePort} on local port ${localPort}.`); const options: IConnectionOptions = { commit: this.productService.commit, - socketFactory: nodeSocketFactory, + socketFactory: this.socketFactory, addressProvider, signService: this.signService, logService: this.logService, @@ -165,8 +161,19 @@ export class TunnelService extends AbstractTunnelService { }; const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort); + this.logService.trace('Tunnel created without provider.'); this.addTunnelToMap(remoteHost, remotePort, tunnel); return tunnel; } } } + +export class TunnelService extends BaseTunnelService { + public constructor( + @ILogService logService: ILogService, + @ISignService signService: ISignService, + @IProductService productService: IProductService + ) { + super(nodeSocketFactory, logService, signService, productService); + } +} diff --git a/src/vs/platform/sharedProcess/node/sharedProcess.ts b/src/vs/platform/sharedProcess/node/sharedProcess.ts new file mode 100644 index 000000000..8a0ea62d7 --- /dev/null +++ b/src/vs/platform/sharedProcess/node/sharedProcess.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { LogLevel } from 'vs/platform/log/common/log'; + +export interface ISharedProcess { + + /** + * Toggles the visibility of the otherwise hidden + * shared process window. + */ + toggle(): Promise; +} + +export interface ISharedProcessConfiguration { + readonly machineId: string; + readonly windowId: number; + + readonly appRoot: string; + + readonly userEnv: NodeJS.ProcessEnv; + + readonly sharedIPCHandle: string; + + readonly args: NativeParsedArgs; + + readonly logLevel: LogLevel; + + readonly nodeCachedDataDir?: string; + readonly backupWorkspacesPath: string; +} diff --git a/src/vs/platform/state/node/state.ts b/src/vs/platform/state/node/state.ts index b07335f95..9fd896f91 100644 --- a/src/vs/platform/state/node/state.ts +++ b/src/vs/platform/state/node/state.ts @@ -12,6 +12,8 @@ export interface IStateService { getItem(key: string, defaultValue: T): T; getItem(key: string, defaultValue?: T): T | undefined; + setItem(key: string, data?: object | string | number | boolean | undefined | null): void; + removeItem(key: string): void; } diff --git a/src/vs/platform/state/node/stateService.ts b/src/vs/platform/state/node/stateService.ts index 0def1cf9c..cb46b77fa 100644 --- a/src/vs/platform/state/node/stateService.ts +++ b/src/vs/platform/state/node/stateService.ts @@ -143,7 +143,7 @@ export class StateService implements IStateService { } getItem(key: string, defaultValue: T): T; - getItem(key: string, defaultValue: T | undefined): T | undefined; + getItem(key: string, defaultValue?: T): T | undefined; getItem(key: string, defaultValue?: T): T | undefined { return this.fileStorage.getItem(key, defaultValue); } diff --git a/src/vs/platform/state/test/node/state.test.ts b/src/vs/platform/state/test/node/state.test.ts index 82166e204..dc4acc6ae 100644 --- a/src/vs/platform/state/test/node/state.test.ts +++ b/src/vs/platform/state/test/node/state.test.ts @@ -4,31 +4,37 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { tmpdir } from 'os'; +import { join } from 'vs/base/common/path'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { FileStorage } from 'vs/platform/state/node/stateService'; -import { mkdirp, rimraf, RimRafMode, writeFileSync } from 'vs/base/node/pfs'; +import { mkdirp, rimraf, writeFileSync } from 'vs/base/node/pfs'; -suite('StateService', () => { - const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'stateservice'); - const storageFile = path.join(parentDir, 'storage.json'); +flakySuite('StateService', () => { - teardown(async () => { - await rimraf(parentDir, RimRafMode.MOVE); + let testDir: string; + + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'stateservice'); + + return mkdirp(testDir); }); - test('Basics', async () => { - await mkdirp(parentDir); + teardown(() => { + return rimraf(testDir); + }); + + test('Basics', async function () { + const storageFile = join(testDir, 'storage.json'); writeFileSync(storageFile, ''); let service = new FileStorage(storageFile, () => null); service.setItem('some.key', 'some.value'); - assert.equal(service.getItem('some.key'), 'some.value'); + assert.strictEqual(service.getItem('some.key'), 'some.value'); service.removeItem('some.key'); - assert.equal(service.getItem('some.key', 'some.default'), 'some.default'); + assert.strictEqual(service.getItem('some.key', 'some.default'), 'some.default'); assert.ok(!service.getItem('some.unknonw.key')); @@ -36,15 +42,15 @@ suite('StateService', () => { service = new FileStorage(storageFile, () => null); - assert.equal(service.getItem('some.other.key'), 'some.other.value'); + assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); service.setItem('some.other.key', 'some.other.value'); - assert.equal(service.getItem('some.other.key'), 'some.other.value'); + assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); service.setItem('some.undefined.key', undefined); - assert.equal(service.getItem('some.undefined.key', 'some.default'), 'some.default'); + assert.strictEqual(service.getItem('some.undefined.key', 'some.default'), 'some.default'); service.setItem('some.null.key', null); - assert.equal(service.getItem('some.null.key', 'some.default'), 'some.default'); + assert.strictEqual(service.getItem('some.null.key', 'some.default'), 'some.default'); }); -}); \ No newline at end of file +}); diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts index 8645255d9..67195995c 100644 --- a/src/vs/platform/storage/node/storageIpc.ts +++ b/src/vs/platform/storage/node/storageIpc.ts @@ -200,12 +200,10 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS return this.channel.call('updateItems', serializableRequest); } - close(): Promise { + async close(): Promise { // when we are about to close, we start to ignore main-side changes since we close anyway dispose(this.onDidChangeItemsOnMainListener); - - return Promise.resolve(); // global storage is closed on the main side } dispose(): void { diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index 97cb9b161..5be6557bd 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -12,7 +12,7 @@ import { mark } from 'vs/base/common/performance'; import { join } from 'vs/base/common/path'; import { copy, exists, mkdirp, writeFile } from 'vs/base/node/pfs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IWorkspaceInitializationPayload, isWorkspaceIdentifier, isSingleFolderWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; +import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { assertIsDefined } from 'vs/base/common/types'; import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; @@ -83,7 +83,7 @@ export class NativeStorageService extends AbstractStorageService { const useInMemoryStorage = !!this.environmentService.extensionTestsLocationURI; // no storage during extension tests! // Create workspace storage and initialize - mark('willInitWorkspaceStorage'); + mark('code/willInitWorkspaceStorage'); try { const workspaceStorage = this.createWorkspaceStorage( useInMemoryStorage ? SQLiteStorageDatabase.IN_MEMORY_PATH : join(result.path, NativeStorageService.WORKSPACE_STORAGE_NAME), @@ -99,7 +99,7 @@ export class NativeStorageService extends AbstractStorageService { workspaceStorage.set(IS_NEW_KEY, false); } } finally { - mark('didInitWorkspaceStorage'); + mark('code/didInitWorkspaceStorage'); } } catch (error) { this.logService.error(`[storage] initializeWorkspaceStorage(): Unable to init workspace storage due to ${error}`); @@ -148,23 +148,22 @@ export class NativeStorageService extends AbstractStorageService { private ensureWorkspaceStorageFolderMeta(payload: IWorkspaceInitializationPayload): void { let meta: object | undefined = undefined; - if (isSingleFolderWorkspaceInitializationPayload(payload)) { - meta = { folder: payload.folder.toString() }; + if (isSingleFolderWorkspaceIdentifier(payload)) { + meta = { folder: payload.uri.toString() }; } else if (isWorkspaceIdentifier(payload)) { - meta = { configuration: payload.configPath }; + meta = { workspace: payload.configPath.toString() }; } if (meta) { - const logService = this.logService; - const workspaceStorageMetaPath = join(this.getWorkspaceStorageFolderPath(payload), NativeStorageService.WORKSPACE_META_NAME); - (async function () { + (async () => { try { + const workspaceStorageMetaPath = join(this.getWorkspaceStorageFolderPath(payload), NativeStorageService.WORKSPACE_META_NAME); const storageExists = await exists(workspaceStorageMetaPath); if (!storageExists) { await writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2)); } } catch (error) { - logService.error(error); + this.logService.error(error); } })(); } diff --git a/src/vs/platform/storage/test/common/storageService.test.ts b/src/vs/platform/storage/test/common/storageService.test.ts index 886efb2f8..182ff12af 100644 --- a/src/vs/platform/storage/test/common/storageService.test.ts +++ b/src/vs/platform/storage/test/common/storageService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { strictEqual, ok, equal } from 'assert'; +import { strictEqual, ok } from 'assert'; import { StorageScope, InMemoryStorageService, StorageTarget, IStorageValueChangeEvent, IStorageTargetChangeEvent } from 'vs/platform/storage/common/storage'; suite('StorageService', function () { @@ -32,15 +32,15 @@ suite('StorageService', function () { storage.store('test.get', 'foobar', scope, StorageTarget.MACHINE); strictEqual(storage.get('test.get', scope, (undefined)!), 'foobar'); let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get'); - equal(storageValueChangeEvent?.scope, scope); - equal(storageValueChangeEvent?.key, 'test.get'); + strictEqual(storageValueChangeEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.key, 'test.get'); storageValueChangeEvents = []; storage.store('test.get', '', scope, StorageTarget.MACHINE); strictEqual(storage.get('test.get', scope, (undefined)!), ''); storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get'); - equal(storageValueChangeEvent!.scope, scope); - equal(storageValueChangeEvent!.key, 'test.get'); + strictEqual(storageValueChangeEvent!.scope, scope); + strictEqual(storageValueChangeEvent!.key, 'test.get'); storage.store('test.getNumber', 5, scope, StorageTarget.MACHINE); strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 5); @@ -79,8 +79,8 @@ suite('StorageService', function () { storage.remove('test.remove', scope); ok(!storage.get('test.remove', scope, (undefined)!)); let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.remove'); - equal(storageValueChangeEvent?.scope, scope); - equal(storageValueChangeEvent?.key, 'test.remove'); + strictEqual(storageValueChangeEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.key, 'test.remove'); } test('Keys (in-memory)', () => { @@ -107,20 +107,20 @@ suite('StorageService', function () { storage.store('test.target1', 'value1', scope, target); strictEqual(storage.keys(scope, target).length, 1); - equal(storageTargetEvent?.scope, scope); - equal(storageValueChangeEvent?.key, 'test.target1'); - equal(storageValueChangeEvent?.scope, scope); - equal(storageValueChangeEvent?.target, target); + strictEqual(storageTargetEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.key, 'test.target1'); + strictEqual(storageValueChangeEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.target, target); storageTargetEvent = undefined; storageValueChangeEvent = Object.create(null); storage.store('test.target1', 'otherValue1', scope, target); strictEqual(storage.keys(scope, target).length, 1); - equal(storageTargetEvent, undefined); - equal(storageValueChangeEvent?.key, 'test.target1'); - equal(storageValueChangeEvent?.scope, scope); - equal(storageValueChangeEvent?.target, target); + strictEqual(storageTargetEvent, undefined); + strictEqual(storageValueChangeEvent?.key, 'test.target1'); + strictEqual(storageValueChangeEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.target, target); storage.store('test.target2', 'value2', scope, target); storage.store('test.target3', 'value3', scope, target); @@ -142,9 +142,9 @@ suite('StorageService', function () { storage.remove('test.target4', scope); strictEqual(storage.keys(scope, target).length, keysLength); - equal(storageTargetEvent?.scope, scope); - equal(storageValueChangeEvent?.key, 'test.target4'); - equal(storageValueChangeEvent?.scope, scope); + strictEqual(storageTargetEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.key, 'test.target4'); + strictEqual(storageValueChangeEvent?.scope, scope); } } @@ -171,7 +171,7 @@ suite('StorageService', function () { storage.store('test.target1', undefined, scope, target); strictEqual(storage.keys(scope, target).length, 0); - equal(storageTargetEvent?.scope, scope); + strictEqual(storageTargetEvent?.scope, scope); storage.store('test.target1', '', scope, target); strictEqual(storage.keys(scope, target).length, 1); diff --git a/src/vs/platform/storage/test/electron-browser/storage.test.ts b/src/vs/platform/storage/test/electron-browser/storage.test.ts index 4e5e10a4e..56f0a638d 100644 --- a/src/vs/platform/storage/test/electron-browser/storage.test.ts +++ b/src/vs/platform/storage/test/electron-browser/storage.test.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equal } from 'assert'; +import { strictEqual } from 'assert'; import { FileStorageDatabase } from 'vs/platform/storage/browser/storageService'; -import { generateUuid } from 'vs/base/common/uuid'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { rimraf, RimRafMode } from 'vs/base/node/pfs'; +import { rimraf } from 'vs/base/node/pfs'; import { NullLogService } from 'vs/platform/log/common/log'; import { Storage } from 'vs/base/parts/storage/common/storage'; import { URI } from 'vs/base/common/uri'; @@ -20,11 +19,10 @@ import { Schemas } from 'vs/base/common/network'; suite('Storage', () => { - const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice'); + let testDir: string; let fileService: FileService; let fileProvider: DiskFileSystemProvider; - let testDir: string; const disposables = new DisposableStore(); @@ -38,14 +36,13 @@ suite('Storage', () => { disposables.add(fileService.registerProvider(Schemas.file, fileProvider)); disposables.add(fileProvider); - const id = generateUuid(); - testDir = join(parentDir, id); + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice'); }); - teardown(async () => { + teardown(() => { disposables.clear(); - await rimraf(parentDir, RimRafMode.MOVE); + return rimraf(testDir); }); test('File Based Storage', async () => { @@ -57,9 +54,9 @@ suite('Storage', () => { storage.set('barNumber', 55); storage.set('barBoolean', true); - equal(storage.get('bar'), 'foo'); - equal(storage.get('barNumber'), '55'); - equal(storage.get('barBoolean'), 'true'); + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.get('barNumber'), '55'); + strictEqual(storage.get('barBoolean'), 'true'); await storage.close(); @@ -67,17 +64,17 @@ suite('Storage', () => { await storage.init(); - equal(storage.get('bar'), 'foo'); - equal(storage.get('barNumber'), '55'); - equal(storage.get('barBoolean'), 'true'); + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.get('barNumber'), '55'); + strictEqual(storage.get('barBoolean'), 'true'); storage.delete('bar'); storage.delete('barNumber'); storage.delete('barBoolean'); - equal(storage.get('bar', 'undefined'), 'undefined'); - equal(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); - equal(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); + strictEqual(storage.get('bar', 'undefined'), 'undefined'); + strictEqual(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); + strictEqual(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); await storage.close(); @@ -85,8 +82,8 @@ suite('Storage', () => { await storage.init(); - equal(storage.get('bar', 'undefined'), 'undefined'); - equal(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); - equal(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); + strictEqual(storage.get('bar', 'undefined'), 'undefined'); + strictEqual(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); + strictEqual(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); }); }); diff --git a/src/vs/platform/storage/test/node/storageService.test.ts b/src/vs/platform/storage/test/node/storageService.test.ts index e0a410ef6..6677ab24d 100644 --- a/src/vs/platform/storage/test/node/storageService.test.ts +++ b/src/vs/platform/storage/test/node/storageService.test.ts @@ -3,33 +3,33 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equal } from 'assert'; +import { strictEqual } from 'assert'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { NativeStorageService } from 'vs/platform/storage/node/storageService'; -import { generateUuid } from 'vs/base/common/uuid'; -import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs'; +import { mkdirp, rimraf } from 'vs/base/node/pfs'; import { NullLogService } from 'vs/platform/log/common/log'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage'; import { URI } from 'vs/base/common/uri'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; -suite('NativeStorageService', function () { +flakySuite('NativeStorageService', function () { - function uniqueStorageDir(): string { - const id = generateUuid(); + let testDir: string; - return join(tmpdir(), 'vsctests', id, 'storage2', id); - } + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice'); - test('Migrate Data', async () => { + return mkdirp(testDir); + }); - // Given issues such as https://github.com/microsoft/vscode/issues/108113 - // we see random test failures when accessing the native file system. - this.retries(3); - this.timeout(1000 * 20); + teardown(() => { + return rimraf(testDir); + }); + + test('Migrate Data', async function () { class StorageTestEnvironmentService extends NativeEnvironmentService { @@ -46,27 +46,23 @@ suite('NativeStorageService', function () { } } - const storageDir = uniqueStorageDir(); - await mkdirp(storageDir); - - const storage = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(storageDir), storageDir)); + const storage = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(testDir), testDir)); await storage.initialize({ id: String(Date.now()) }); storage.store('bar', 'foo', StorageScope.WORKSPACE, StorageTarget.MACHINE); storage.store('barNumber', 55, StorageScope.WORKSPACE, StorageTarget.MACHINE); storage.store('barBoolean', true, StorageScope.GLOBAL, StorageTarget.MACHINE); - equal(storage.get('bar', StorageScope.WORKSPACE), 'foo'); - equal(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55); - equal(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true); + strictEqual(storage.get('bar', StorageScope.WORKSPACE), 'foo'); + strictEqual(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55); + strictEqual(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true); await storage.migrate({ id: String(Date.now() + 100) }); - equal(storage.get('bar', StorageScope.WORKSPACE), 'foo'); - equal(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55); - equal(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true); + strictEqual(storage.get('bar', StorageScope.WORKSPACE), 'foo'); + strictEqual(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55); + strictEqual(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true); await storage.close(); - await rimraf(storageDir, RimRafMode.MOVE); }); }); diff --git a/src/vs/platform/telemetry/node/commonProperties.ts b/src/vs/platform/telemetry/common/commonProperties.ts similarity index 78% rename from src/vs/platform/telemetry/node/commonProperties.ts rename to src/vs/platform/telemetry/common/commonProperties.ts index d681c0c77..46f08b5d3 100644 --- a/src/vs/platform/telemetry/node/commonProperties.ts +++ b/src/vs/platform/telemetry/common/commonProperties.ts @@ -3,12 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as Platform from 'vs/base/common/platform'; -import * as os from 'os'; -import * as uuid from 'vs/base/common/uuid'; -import { readFile } from 'vs/base/node/pfs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { isLinuxSnap, PlatformToString, platform } from 'vs/base/common/platform'; +import { platform as nodePlatform, env } from 'vs/base/common/process'; +import { generateUuid } from 'vs/base/common/uuid'; +import { URI } from 'vs/base/common/uri'; export async function resolveCommonProperties( + fileService: IFileService, + release: string, + arch: string, commit: string | undefined, version: string | undefined, machineId: string | undefined, @@ -21,19 +25,19 @@ export async function resolveCommonProperties( // __GDPR__COMMON__ "common.machineId" : { "endPoint": "MacAddressHash", "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" } result['common.machineId'] = machineId; // __GDPR__COMMON__ "sessionID" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - result['sessionID'] = uuid.generateUuid() + Date.now(); + result['sessionID'] = generateUuid() + Date.now(); // __GDPR__COMMON__ "commitHash" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } result['commitHash'] = commit; // __GDPR__COMMON__ "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['version'] = version; // __GDPR__COMMON__ "common.platformVersion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - result['common.platformVersion'] = (os.release() || '').replace(/^(\d+)(\.\d+)?(\.\d+)?(.*)/, '$1$2$3'); + result['common.platformVersion'] = (release || '').replace(/^(\d+)(\.\d+)?(\.\d+)?(.*)/, '$1$2$3'); // __GDPR__COMMON__ "common.platform" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - result['common.platform'] = Platform.PlatformToString(Platform.platform); + result['common.platform'] = PlatformToString(platform); // __GDPR__COMMON__ "common.nodePlatform" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - result['common.nodePlatform'] = process.platform; + result['common.nodePlatform'] = nodePlatform; // __GDPR__COMMON__ "common.nodeArch" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - result['common.nodeArch'] = process.arch; + result['common.nodeArch'] = arch; // __GDPR__COMMON__ "common.product" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } result['common.product'] = product || 'desktop'; @@ -64,16 +68,16 @@ export async function resolveCommonProperties( } }); - if (process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION) { + if (isLinuxSnap) { // __GDPR__COMMON__ "common.snap" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['common.snap'] = 'true'; } try { - const contents = await readFile(installSourcePath, 'utf8'); + const contents = await fileService.readFile(URI.file(installSourcePath)); // __GDPR__COMMON__ "common.source" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - result['common.source'] = contents.slice(0, 30); + result['common.source'] = contents.value.toString().slice(0, 30); } catch (error) { // ignore error } @@ -82,10 +86,11 @@ export async function resolveCommonProperties( } function verifyMicrosoftInternalDomain(domainList: readonly string[]): boolean { - if (!process || !process.env || !process.env['USERDNSDOMAIN']) { + const userDnsDomain = env['USERDNSDOMAIN']; + if (!userDnsDomain) { return false; } - const domain = process.env['USERDNSDOMAIN']!.toLowerCase(); + const domain = userDnsDomain.toLowerCase(); return domainList.some(msftDomain => domain === msftDomain); } diff --git a/src/vs/platform/telemetry/node/telemetryIpc.ts b/src/vs/platform/telemetry/common/telemetryIpc.ts similarity index 100% rename from src/vs/platform/telemetry/node/telemetryIpc.ts rename to src/vs/platform/telemetry/common/telemetryIpc.ts diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index b5239353a..7bf7747c4 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -332,6 +332,12 @@ export const editorHoverStatusBarBackground = registerColor('editorHoverWidget.s */ export const editorActiveLinkForeground = registerColor('editorLink.activeForeground', { dark: '#4E94CE', light: Color.blue, hc: Color.cyan }, nls.localize('activeLinkForeground', 'Color of active links.')); +/** + * Inline hints + */ +export const editorInlineHintForeground = registerColor('editorInlineHint.foreground', { dark: editorWidgetBackground, light: editorWidgetForeground, hc: editorWidgetBackground }, nls.localize('editorInlineHintForeground', 'Foreground color of inline hints')); +export const editorInlineHintBackground = registerColor('editorInlineHint.background', { dark: editorWidgetForeground, light: editorWidgetBackground, hc: editorWidgetForeground }, nls.localize('editorInlineHintBackground', 'Background color of inline hints')); + /** * Editor lighbulb icon colors */ diff --git a/src/vs/platform/theme/common/iconRegistry.ts b/src/vs/platform/theme/common/iconRegistry.ts index a19a41a84..4079cc4e2 100644 --- a/src/vs/platform/theme/common/iconRegistry.ts +++ b/src/vs/platform/theme/common/iconRegistry.ts @@ -12,7 +12,6 @@ import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/plat import { RunOnceScheduler } from 'vs/base/common/async'; import * as Codicons from 'vs/base/common/codicons'; - // ------ API types @@ -190,11 +189,6 @@ class IconRegistry implements IIconRegistry { public toString() { const sorter = (i1: IconContribution, i2: IconContribution) => { - const isThemeIcon1 = ThemeIcon.isThemeIcon(i1.defaults); - const isThemeIcon2 = ThemeIcon.isThemeIcon(i2.defaults); - if (isThemeIcon1 !== isThemeIcon2) { - return isThemeIcon1 ? -1 : 1; - } return i1.id.localeCompare(i2.id); }; const classNames = (i: IconContribution) => { @@ -205,18 +199,24 @@ class IconRegistry implements IIconRegistry { }; let reference = []; - let docCss = []; + reference.push(`| preview | identifier | default codicon id | description`); + reference.push(`| ----------- | --------------------------------- | --------------------------------- | --------------------------------- |`); const contributions = Object.keys(this.iconsById).map(key => this.iconsById[key]); - for (const i of contributions.sort(sorter)) { - reference.push(`||${i.id}|${ThemeIcon.isThemeIcon(i.defaults) ? i.defaults.id : ''}|${i.description || ''}|`); - - if (!ThemeIcon.isThemeIcon((i.defaults))) { - docCss.push(`.codicon-${i.id}:before { content: "${i.defaults.character}" }`); - } + for (const i of contributions.filter(i => !!i.description).sort(sorter)) { + reference.push(`||${i.id}|${ThemeIcon.isThemeIcon(i.defaults) ? i.defaults.id : i.id}|${i.description || ''}|`); } - return reference.join('\n') + '\n\n' + docCss.join('\n'); + + reference.push(`| preview | identifier `); + reference.push(`| ----------- | --------------------------------- |`); + + for (const i of contributions.filter(i => !ThemeIcon.isThemeIcon(i.defaults)).sort(sorter)) { + reference.push(`||${i.id}|`); + + } + + return reference.join('\n'); } } @@ -262,3 +262,5 @@ export const widgetClose = registerIcon('widget-close', Codicons.Codicon.close, export const gotoPreviousLocation = registerIcon('goto-previous-location', Codicons.Codicon.arrowUp, localize('previousChangeIcon', 'Icon for goto previous editor location.')); export const gotoNextLocation = registerIcon('goto-next-location', Codicons.Codicon.arrowDown, localize('nextChangeIcon', 'Icon for goto next editor location.')); + +export const syncing = ThemeIcon.modify(Codicons.Codicon.sync, 'spin'); diff --git a/src/vs/platform/theme/common/themeService.ts b/src/vs/platform/theme/common/themeService.ts index f82c45767..1096c6be5 100644 --- a/src/vs/platform/theme/common/themeService.ts +++ b/src/vs/platform/theme/common/themeService.ts @@ -11,7 +11,7 @@ import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; import { Event, Emitter } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { CSSIcon } from 'vs/base/common/codicons'; +import { Codicon, CSSIcon } from 'vs/base/common/codicons'; export const IThemeService = createDecorator('themeService'); @@ -70,50 +70,13 @@ export namespace ThemeIcon { return ti1.id === ti2.id && ti1.color?.id === ti2.color?.id; } - const _regexAsClassName = /^(codicon\/)?([a-z-]+)(~[a-z]+)?$/i; - - export function asClassNameArray(icon: ThemeIcon): string[] { - const match = _regexAsClassName.exec(icon.id); - if (!match) { - return ['codicon', 'codicon-error']; - } - let [, , name, modifier] = match; - let className = `codicon-${name}`; - if (modifier) { - return ['codicon', className, modifier.substr(1)]; - } - return ['codicon', className]; - } - - - export function asClassName(icon: ThemeIcon): string { - return asClassNameArray(icon).join(' '); - } - - export function asCSSSelector(icon: ThemeIcon): string { - return '.' + asClassNameArray(icon).join('.'); - } - - export function asCSSIcon(icon: ThemeIcon): CSSIcon { - return { - classNames: asClassName(icon) - }; - } - - export function asCodiconLabel(icon: ThemeIcon): string { - return '$(' + icon.id + ')'; - } - - export function revive(icon: any): ThemeIcon | undefined { - if (ThemeIcon.isThemeIcon(icon)) { - return { id: icon.id, color: icon.color ? { id: icon.color.id } : undefined }; - } - return undefined; - } + export const asClassNameArray: (icon: ThemeIcon) => string[] = CSSIcon.asClassNameArray; + export const asClassName: (icon: ThemeIcon) => string = CSSIcon.asClassName; + export const asCSSSelector: (icon: ThemeIcon) => string = CSSIcon.asCSSSelector; } -export const FileThemeIcon = { id: 'file' }; -export const FolderThemeIcon = { id: 'folder' }; +export const FileThemeIcon = Codicon.file; +export const FolderThemeIcon = Codicon.folder; export function getThemeTypeSelector(type: ColorScheme): string { switch (type) { diff --git a/src/vs/platform/theme/test/common/testThemeService.ts b/src/vs/platform/theme/test/common/testThemeService.ts index 2bdd34d8e..e21487ef0 100644 --- a/src/vs/platform/theme/test/common/testThemeService.ts +++ b/src/vs/platform/theme/test/common/testThemeService.ts @@ -12,8 +12,11 @@ export class TestColorTheme implements IColorTheme { public readonly label = 'test'; - constructor(private colors: { [id: string]: string; } = {}, public type = ColorScheme.DARK) { - } + constructor( + private colors: { [id: string]: string; } = {}, + public type = ColorScheme.DARK, + public readonly semanticHighlighting = false + ) { } getColor(color: string, useDefault?: boolean): Color | undefined { let value = this.colors[color]; @@ -31,8 +34,6 @@ export class TestColorTheme implements IColorTheme { return undefined; } - readonly semanticHighlighting = false; - get tokenColorMap(): string[] { return []; } diff --git a/src/vs/platform/undoRedo/common/undoRedo.ts b/src/vs/platform/undoRedo/common/undoRedo.ts index 021003bab..80512775d 100644 --- a/src/vs/platform/undoRedo/common/undoRedo.ts +++ b/src/vs/platform/undoRedo/common/undoRedo.ts @@ -18,6 +18,10 @@ export interface IResourceUndoRedoElement { readonly type: UndoRedoElementType.Resource; readonly resource: URI; readonly label: string; + /** + * Show a message to the user confirming when trying to undo this element + */ + readonly confirmBeforeUndo?: boolean; undo(): Promise | void; redo(): Promise | void; } @@ -26,6 +30,10 @@ export interface IWorkspaceUndoRedoElement { readonly type: UndoRedoElementType.Workspace; readonly resources: readonly URI[]; readonly label: string; + /** + * Show a message to the user confirming when trying to undo this element + */ + readonly confirmBeforeUndo?: boolean; undo(): Promise | void; redo(): Promise | void; diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index 2fb802fbc..7e205e707 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -27,6 +27,7 @@ class ResourceStackElement { public readonly type = UndoRedoElementType.Resource; public readonly actual: IUndoRedoElement; public readonly label: string; + public readonly confirmBeforeUndo: boolean; public readonly resourceLabel: string; public readonly strResource: string; @@ -41,6 +42,7 @@ class ResourceStackElement { constructor(actual: IUndoRedoElement, resourceLabel: string, strResource: string, groupId: number, groupOrder: number, sourceId: number, sourceOrder: number) { this.actual = actual; this.label = actual.label; + this.confirmBeforeUndo = actual.confirmBeforeUndo || false; this.resourceLabel = resourceLabel; this.strResource = strResource; this.resourceLabels = [this.resourceLabel]; @@ -129,6 +131,7 @@ class WorkspaceStackElement { public readonly type = UndoRedoElementType.Workspace; public readonly actual: IWorkspaceUndoRedoElement; public readonly label: string; + public readonly confirmBeforeUndo: boolean; public readonly resourceLabels: string[]; public readonly strResources: string[]; @@ -142,6 +145,7 @@ class WorkspaceStackElement { constructor(actual: IWorkspaceUndoRedoElement, resourceLabels: string[], strResources: string[], groupId: number, groupOrder: number, sourceId: number, sourceOrder: number) { this.actual = actual; this.label = actual.label; + this.confirmBeforeUndo = actual.confirmBeforeUndo || false; this.resourceLabels = resourceLabels; this.strResources = strResources; this.groupId = groupId; @@ -811,7 +815,7 @@ export class UndoRedoService implements IUndoRedoService { if (element.canSplit()) { this._splitPastWorkspaceElement(element, ignoreResources); this._notificationService.info(message); - return new WorkspaceVerificationError(this._undo(strResource)); + return new WorkspaceVerificationError(this._undo(strResource, 0, true)); } else { // Cannot safely split this workspace element => flush all undo/redo stacks for (const strResource of element.strResources) { @@ -899,13 +903,13 @@ export class UndoRedoService implements IUndoRedoService { return null; } - private _workspaceUndo(strResource: string, element: WorkspaceStackElement): Promise | void { + private _workspaceUndo(strResource: string, element: WorkspaceStackElement, undoConfirmed: boolean): Promise | void { const affectedEditStacks = this._getAffectedEditStacks(element); const verificationError = this._checkWorkspaceUndo(strResource, element, affectedEditStacks, /*invalidated resources will be checked after the prepare call*/false); if (verificationError) { return verificationError.returnValue; } - return this._confirmAndExecuteWorkspaceUndo(strResource, element, affectedEditStacks); + return this._confirmAndExecuteWorkspaceUndo(strResource, element, affectedEditStacks, undoConfirmed); } private _isPartOfUndoGroup(element: WorkspaceStackElement): boolean { @@ -933,7 +937,7 @@ export class UndoRedoService implements IUndoRedoService { return false; } - private async _confirmAndExecuteWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot): Promise { + private async _confirmAndExecuteWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, undoConfirmed: boolean): Promise { if (element.canSplit() && !this._isPartOfUndoGroup(element)) { // this element can be split @@ -959,7 +963,7 @@ export class UndoRedoService implements IUndoRedoService { if (result.choice === 1) { // choice: undo this file this._splitPastWorkspaceElement(element, null); - return this._undo(strResource); + return this._undo(strResource, 0, true); } // choice: undo in all files @@ -969,6 +973,8 @@ export class UndoRedoService implements IUndoRedoService { if (verificationError1) { return verificationError1.returnValue; } + + undoConfirmed = true; } // prepare @@ -989,10 +995,10 @@ export class UndoRedoService implements IUndoRedoService { for (const editStack of editStackSnapshot.editStacks) { editStack.moveBackward(element); } - return this._safeInvokeWithLocks(element, () => element.actual.undo(), editStackSnapshot, cleanup, () => this._continueUndoInGroup(element.groupId)); + return this._safeInvokeWithLocks(element, () => element.actual.undo(), editStackSnapshot, cleanup, () => this._continueUndoInGroup(element.groupId, undoConfirmed)); } - private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement): Promise | void { + private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement, undoConfirmed: boolean): Promise | void { if (!element.isValid) { // invalid element => immediately flush edit stack! editStack.flushAllElements(); @@ -1008,7 +1014,7 @@ export class UndoRedoService implements IUndoRedoService { } return this._invokeResourcePrepare(element, (cleanup) => { editStack.moveBackward(element); - return this._safeInvokeWithLocks(element, () => element.actual.undo(), new EditStackSnapshot([editStack]), cleanup, () => this._continueUndoInGroup(element.groupId)); + return this._safeInvokeWithLocks(element, () => element.actual.undo(), new EditStackSnapshot([editStack]), cleanup, () => this._continueUndoInGroup(element.groupId, undoConfirmed)); }); } @@ -1037,29 +1043,29 @@ export class UndoRedoService implements IUndoRedoService { return [matchedElement, matchedStrResource]; } - private _continueUndoInGroup(groupId: number): Promise | void { + private _continueUndoInGroup(groupId: number, undoConfirmed: boolean): Promise | void { if (!groupId) { return; } const [, matchedStrResource] = this._findClosestUndoElementInGroup(groupId); if (matchedStrResource) { - return this._undo(matchedStrResource); + return this._undo(matchedStrResource, 0, undoConfirmed); } } public undo(resourceOrSource: URI | UndoRedoSource): Promise | void { if (resourceOrSource instanceof UndoRedoSource) { const [, matchedStrResource] = this._findClosestUndoElementWithSource(resourceOrSource.id); - return matchedStrResource ? this._undo(matchedStrResource, resourceOrSource.id) : undefined; + return matchedStrResource ? this._undo(matchedStrResource, resourceOrSource.id, false) : undefined; } if (typeof resourceOrSource === 'string') { - return this._undo(resourceOrSource); + return this._undo(resourceOrSource, 0, false); } - return this._undo(this.getUriComparisonKey(resourceOrSource)); + return this._undo(this.getUriComparisonKey(resourceOrSource), 0, false); } - private _undo(strResource: string, sourceId: number = 0): Promise | void { + private _undo(strResource: string, sourceId: number = 0, undoConfirmed: boolean): Promise | void { if (!this._editStacks.has(strResource)) { return; } @@ -1075,20 +1081,21 @@ export class UndoRedoService implements IUndoRedoService { const [matchedElement, matchedStrResource] = this._findClosestUndoElementInGroup(element.groupId); if (element !== matchedElement && matchedStrResource) { // there is an element in the same group that should be undone before this one - return this._undo(matchedStrResource); + return this._undo(matchedStrResource, sourceId, undoConfirmed); } } - if (element.sourceId !== sourceId) { - // Hit a different source, prompt for confirmation - return this._confirmDifferentSourceAndContinueUndo(strResource, element); + const shouldPromptForConfirmation = (element.sourceId !== sourceId || element.confirmBeforeUndo); + if (shouldPromptForConfirmation && !undoConfirmed) { + // Hit a different source or the element asks for prompt before undo, prompt for confirmation + return this._confirmAndContinueUndo(strResource, sourceId, element); } try { if (element.type === UndoRedoElementType.Workspace) { - return this._workspaceUndo(strResource, element); + return this._workspaceUndo(strResource, element, undoConfirmed); } else { - return this._resourceUndo(editStack, element); + return this._resourceUndo(editStack, element, undoConfirmed); } } finally { if (DEBUG) { @@ -1097,7 +1104,7 @@ export class UndoRedoService implements IUndoRedoService { } } - private async _confirmDifferentSourceAndContinueUndo(strResource: string, element: StackElement): Promise { + private async _confirmAndContinueUndo(strResource: string, sourceId: number, element: StackElement): Promise { const result = await this._dialogService.show( Severity.Info, nls.localize('confirmDifferentSource', "Would you like to undo '{0}'?", element.label), @@ -1116,7 +1123,7 @@ export class UndoRedoService implements IUndoRedoService { } // choice: undo - return this._undo(strResource, element.sourceId); + return this._undo(strResource, sourceId, true); } private _findClosestRedoElementWithSource(sourceId: number): [StackElement | null, string | null] { diff --git a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts index 4c0a48e58..49f494454 100644 --- a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts +++ b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts @@ -23,9 +23,9 @@ suite('UndoRedoService', () => { const resource = URI.file('test.txt'); const service = createUndoRedoService(); - assert.equal(service.canUndo(resource), false); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), false); + assert.strictEqual(service.canUndo(resource), false); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), false); assert.ok(service.getLastElement(resource) === null); let undoCall1 = 0; @@ -39,27 +39,27 @@ suite('UndoRedoService', () => { }; service.pushElement(element1); - assert.equal(undoCall1, 0); - assert.equal(redoCall1, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 0); + assert.strictEqual(redoCall1, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === element1); service.undo(resource); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 0); - assert.equal(service.canUndo(resource), false); - assert.equal(service.canRedo(resource), true); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 0); + assert.strictEqual(service.canUndo(resource), false); + assert.strictEqual(service.canRedo(resource), true); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === null); service.redo(resource); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === element1); let undoCall2 = 0; @@ -73,24 +73,24 @@ suite('UndoRedoService', () => { }; service.pushElement(element2); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall2, 0); - assert.equal(redoCall2, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall2, 0); + assert.strictEqual(redoCall2, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === element2); service.undo(resource); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall2, 1); - assert.equal(redoCall2, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), true); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall2, 1); + assert.strictEqual(redoCall2, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), true); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === null); let undoCall3 = 0; @@ -104,28 +104,28 @@ suite('UndoRedoService', () => { }; service.pushElement(element3); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall2, 1); - assert.equal(redoCall2, 0); - assert.equal(undoCall3, 0); - assert.equal(redoCall3, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall2, 1); + assert.strictEqual(redoCall2, 0); + assert.strictEqual(undoCall3, 0); + assert.strictEqual(redoCall3, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === element3); service.undo(resource); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall2, 1); - assert.equal(redoCall2, 0); - assert.equal(undoCall3, 1); - assert.equal(redoCall3, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), true); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall2, 1); + assert.strictEqual(redoCall2, 0); + assert.strictEqual(undoCall3, 1); + assert.strictEqual(redoCall3, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), true); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === null); }); @@ -169,50 +169,50 @@ suite('UndoRedoService', () => { }; service.pushElement(element1); - assert.equal(service.canUndo(resource1), true); - assert.equal(service.canRedo(resource1), false); - assert.equal(service.hasElements(resource1), true); + assert.strictEqual(service.canUndo(resource1), true); + assert.strictEqual(service.canRedo(resource1), false); + assert.strictEqual(service.hasElements(resource1), true); assert.ok(service.getLastElement(resource1) === element1); - assert.equal(service.canUndo(resource2), true); - assert.equal(service.canRedo(resource2), false); - assert.equal(service.hasElements(resource2), true); + assert.strictEqual(service.canUndo(resource2), true); + assert.strictEqual(service.canRedo(resource2), false); + assert.strictEqual(service.hasElements(resource2), true); assert.ok(service.getLastElement(resource2) === element1); await service.undo(resource1); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 0); - assert.equal(service.canUndo(resource1), false); - assert.equal(service.canRedo(resource1), true); - assert.equal(service.hasElements(resource1), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 0); + assert.strictEqual(service.canUndo(resource1), false); + assert.strictEqual(service.canRedo(resource1), true); + assert.strictEqual(service.hasElements(resource1), true); assert.ok(service.getLastElement(resource1) === null); - assert.equal(service.canUndo(resource2), false); - assert.equal(service.canRedo(resource2), true); - assert.equal(service.hasElements(resource2), true); + assert.strictEqual(service.canUndo(resource2), false); + assert.strictEqual(service.canRedo(resource2), true); + assert.strictEqual(service.hasElements(resource2), true); assert.ok(service.getLastElement(resource2) === null); await service.redo(resource2); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall11, 0); - assert.equal(redoCall11, 0); - assert.equal(undoCall12, 0); - assert.equal(redoCall12, 0); - assert.equal(service.canUndo(resource1), true); - assert.equal(service.canRedo(resource1), false); - assert.equal(service.hasElements(resource1), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall11, 0); + assert.strictEqual(redoCall11, 0); + assert.strictEqual(undoCall12, 0); + assert.strictEqual(redoCall12, 0); + assert.strictEqual(service.canUndo(resource1), true); + assert.strictEqual(service.canRedo(resource1), false); + assert.strictEqual(service.hasElements(resource1), true); assert.ok(service.getLastElement(resource1) === element1); - assert.equal(service.canUndo(resource2), true); - assert.equal(service.canRedo(resource2), false); - assert.equal(service.hasElements(resource2), true); + assert.strictEqual(service.canUndo(resource2), true); + assert.strictEqual(service.canRedo(resource2), false); + assert.strictEqual(service.hasElements(resource2), true); assert.ok(service.getLastElement(resource2) === element1); }); test('UndoRedoGroup.None uses id 0', () => { - assert.equal(UndoRedoGroup.None.id, 0); - assert.equal(UndoRedoGroup.None.nextOrder(), 0); - assert.equal(UndoRedoGroup.None.nextOrder(), 0); + assert.strictEqual(UndoRedoGroup.None.id, 0); + assert.strictEqual(UndoRedoGroup.None.nextOrder(), 0); + assert.strictEqual(UndoRedoGroup.None.nextOrder(), 0); }); }); diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 2e18cc8cd..ddfa1ee4a 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -15,6 +15,7 @@ import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/e import { ILogService } from 'vs/platform/log/common/log'; import { AbstractUpdateService, createUpdateURL, UpdateNotAvailableClassification } from 'vs/platform/update/electron-main/abstractUpdateService'; import { IRequestService } from 'vs/platform/request/common/request'; +import product from 'vs/platform/product/common/product'; export class DarwinUpdateService extends AbstractUpdateService { @@ -56,7 +57,12 @@ export class DarwinUpdateService extends AbstractUpdateService { } protected buildUpdateFeedUrl(quality: string): string | undefined { - const assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64'; + let assetID: string; + if (!product.darwinUniversalAssetId) { + assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64'; + } else { + assetID = product.darwinUniversalAssetId; + } const url = createUpdateURL(assetID, quality); try { electron.autoUpdater.setFeedURL({ url }); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 3ffa7bf7b..d1c834165 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -56,7 +56,7 @@ export class Win32UpdateService extends AbstractUpdateService { @memoize get cachePath(): Promise { const result = path.join(tmpdir(), `vscode-update-${product.target}-${process.arch}`); - return pfs.mkdirp(result, undefined).then(() => result); + return pfs.mkdirp(result).then(() => result); } constructor( diff --git a/src/vs/platform/userDataSync/common/extensionsStorageSync.ts b/src/vs/platform/userDataSync/common/extensionsStorageSync.ts index 37846e053..3e8f5400e 100644 --- a/src/vs/platform/userDataSync/common/extensionsStorageSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsStorageSync.ts @@ -5,6 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; +import { adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -32,7 +33,7 @@ export class ExtensionsStorageSyncService extends Disposable implements IExtensi declare readonly _serviceBrand: undefined; private static toKey(extension: IExtensionIdWithVersion): string { - return `extensionKeys/${extension.id}@${extension.version}`; + return `extensionKeys/${adoptToGalleryExtensionId(extension.id)}@${extension.version}`; } private static fromKey(key: string): IExtensionIdWithVersion | undefined { diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index e716e1355..74fe8d9c0 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -11,7 +11,7 @@ import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { areSameExtensions, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IFileService } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; @@ -25,7 +25,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { CancellationToken } from 'vs/base/common/cancellation'; import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { getErrorMessage } from 'vs/base/common/errors'; -import { forEach, IStringDictionary } from 'vs/base/common/collections'; +import { IStringDictionary } from 'vs/base/common/collections'; import { IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync'; interface IExtensionResourceMergeResult extends IAcceptResult { @@ -73,6 +73,15 @@ async function parseAndMigrateExtensions(syncData: ISyncData, extensionManagemen return extensions; } +function getExtensionStorageState(publisher: string, name: string, storageService: IStorageService): IStringDictionary { + const extensionStorageValue = storageService.get(getExtensionId(publisher, name) /* use the same id used in extension host */, StorageScope.GLOBAL) || '{}'; + return JSON.parse(extensionStorageValue); +} + +function storeExtensionStorageState(publisher: string, name: string, extensionState: IStringDictionary, storageService: IStorageService): void { + storageService.store(getExtensionId(publisher, name) /* use the same id used in extension host */, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE); +} + export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/extensions.json` }); @@ -99,7 +108,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService, - @IIgnoredExtensionsManagementService private readonly extensionSyncManagementService: IIgnoredExtensionsManagementService, + @IIgnoredExtensionsManagementService private readonly ignoredExtensionsManagementService: IIgnoredExtensionsManagementService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IConfigurationService configurationService: IConfigurationService, @@ -125,7 +134,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const installedExtensions = await this.extensionManagementService.getInstalled(); const localExtensions = this.getLocalExtensions(installedExtensions); - const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); + const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); if (remoteExtensions) { this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`); @@ -209,7 +218,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse private async acceptLocal(resourcePreview: IExtensionResourcePreview): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); - const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); + const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); const mergeResult = merge(resourcePreview.localExtensions, null, null, resourcePreview.skippedExtensions, ignoredExtensions); const { added, removed, updated, remote } = mergeResult; return { @@ -225,7 +234,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse private async acceptRemote(resourcePreview: IExtensionResourcePreview): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); - const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); + const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null; if (remoteExtensions !== null) { const mergeResult = merge(resourcePreview.localExtensions, remoteExtensions, resourcePreview.localExtensions, [], ignoredExtensions); @@ -285,7 +294,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse async resolveContent(uri: URI): Promise { if (this.extUri.isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) { const installedExtensions = await this.extensionManagementService.getInstalled(); - const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); + const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); const localExtensions = this.getLocalExtensions(installedExtensions).filter(e => !ignoredExtensions.some(id => areSameExtensions({ id }, e.identifier))); return this.format(localExtensions); } @@ -363,7 +372,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse // Builtin Extension Sync: Enablement & State if (installedExtension && installedExtension.isBuiltin) { if (e.state && installedExtension.manifest.version === e.version) { - this.updateExtensionState(e.state, e.identifier.id, installedExtension.manifest.version); + this.updateExtensionState(e.state, installedExtension.manifest.publisher, installedExtension.manifest.name, installedExtension.manifest.version); } if (e.disabled) { this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id); @@ -382,14 +391,16 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const extension = await this.extensionGalleryService.getCompatibleExtension(e.identifier); /* Update extension state only if - * extension is installed and version is same as synced version or - * extension is not installed and installable + * extension is installed and version is same as synced version or + * extension is not installed and installable */ if (e.state && (installedExtension ? installedExtension.manifest.version === e.version /* Installed and has same version */ : !!extension /* Installable */) ) { - this.updateExtensionState(e.state, e.identifier.id, installedExtension?.manifest.version); + const publisher = installedExtension ? installedExtension.manifest.publisher : extension!.publisher; + const name = installedExtension ? installedExtension.manifest.name : extension!.name; + this.updateExtensionState(e.state, publisher, name, installedExtension?.manifest.version); } if (extension) { @@ -436,15 +447,15 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return newSkippedExtensions; } - private updateExtensionState(state: IStringDictionary, id: string, version?: string): void { - const extensionState = JSON.parse(this.storageService.get(id, StorageScope.GLOBAL) || '{}'); - const keys = version ? this.extensionsStorageSyncService.getKeysForSync({ id, version }) : undefined; + private updateExtensionState(state: IStringDictionary, publisher: string, name: string, version: string | undefined): void { + const extensionState = getExtensionStorageState(publisher, name, this.storageService); + const keys = version ? this.extensionsStorageSyncService.getKeysForSync({ id: getGalleryExtensionId(publisher, name), version }) : undefined; if (keys) { - keys.forEach(key => extensionState[key] = state[key]); + keys.forEach(key => { extensionState[key] = state[key]; }); } else { - forEach(state, ({ key, value }) => extensionState[key] = value); + Object.keys(state).forEach(key => extensionState[key] = state[key]); } - this.storageService.store(id, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE); + storeExtensionStorageState(publisher, name, extensionState, this.storageService); } private parseExtensions(syncData: ISyncData): ISyncExtension[] { @@ -465,8 +476,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse try { const keys = this.extensionsStorageSyncService.getKeysForSync({ id: identifier.id, version: manifest.version }); if (keys) { - const extensionStorageValue = this.storageService.get(identifier.id, StorageScope.GLOBAL) || '{}'; - const extensionStorageState = JSON.parse(extensionStorageValue); + const extensionStorageState = getExtensionStorageState(manifest.publisher, manifest.name, this.storageService); syncExntesion.state = Object.keys(extensionStorageState).reduce((state: IStringDictionary, key) => { if (keys.includes(key)) { state[key] = extensionStorageState[key]; @@ -490,6 +500,7 @@ export class ExtensionsInitializer extends AbstractInitializer { @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService, @IStorageService private readonly storageService: IStorageService, + @IIgnoredExtensionsManagementService private readonly ignoredExtensionsManagementService: IIgnoredExtensionsManagementService, @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @@ -511,12 +522,18 @@ export class ExtensionsInitializer extends AbstractInitializer { const newlyEnabledExtensions: ILocalExtension[] = []; const installedExtensions = await this.extensionManagementService.getInstalled(); const newExtensionsToSync = new Map(); - const installedExtensionsToSync: ISyncExtension[] = []; + const installedExtensionsToSync: { syncExtension: ISyncExtension, installedExtension: ILocalExtension }[] = []; const toInstall: { names: string[], uuids: string[] } = { names: [], uuids: [] }; const toDisable: IExtensionIdentifier[] = []; for (const extension of remoteExtensions) { - if (installedExtensions.some(i => areSameExtensions(i.identifier, extension.identifier))) { - installedExtensionsToSync.push(extension); + if (this.ignoredExtensionsManagementService.hasToNeverSyncExtension(extension.identifier.id)) { + // Skip extension ignored to sync + continue; + } + + const installedExtension = installedExtensions.find(i => areSameExtensions(i.identifier, extension.identifier)); + if (installedExtension) { + installedExtensionsToSync.push({ syncExtension: extension, installedExtension }); if (extension.disabled) { toDisable.push(extension.identifier); } @@ -528,17 +545,39 @@ export class ExtensionsInitializer extends AbstractInitializer { } else { toInstall.names.push(extension.identifier.id); } + if (extension.disabled) { + toDisable.push(extension.identifier); + } } } } + // 1. Initialise already installed extensions state + for (const { syncExtension, installedExtension } of installedExtensionsToSync) { + if (syncExtension.state) { + const extensionState = getExtensionStorageState(installedExtension.manifest.publisher, installedExtension.manifest.name, this.storageService); + Object.keys(syncExtension.state).forEach(key => extensionState[key] = syncExtension.state![key]); + storeExtensionStorageState(installedExtension.manifest.publisher, installedExtension.manifest.name, extensionState, this.storageService); + } + } + + // 2. Initialise extensions enablement + if (toDisable.length) { + for (const identifier of toDisable) { + this.logService.trace(`Disabling extension...`, identifier.id); + await this.extensionEnablementService.disableExtension(identifier); + this.logService.info(`Disabling extension`, identifier.id); + } + } + + // 3. Install extensions if (toInstall.names.length || toInstall.uuids.length) { const galleryExtensions = (await this.galleryService.query({ ids: toInstall.uuids, names: toInstall.names, pageSize: toInstall.uuids.length + toInstall.names.length }, CancellationToken.None)).firstPage; for (const galleryExtension of galleryExtensions) { try { const extensionToSync = newExtensionsToSync.get(galleryExtension.identifier.id.toLowerCase())!; if (extensionToSync.state) { - this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionToSync.state), StorageScope.GLOBAL, StorageTarget.MACHINE); + storeExtensionStorageState(galleryExtension.publisher, galleryExtension.name, extensionToSync.state, this.storageService); } this.logService.trace(`Installing extension...`, galleryExtension.identifier.id); const local = await this.extensionManagementService.installFromGallery(galleryExtension, { isMachineScoped: false } /* pass options to prevent install and sync dialog in web */); @@ -552,22 +591,6 @@ export class ExtensionsInitializer extends AbstractInitializer { } } - if (toDisable.length) { - for (const identifier of toDisable) { - this.logService.trace(`Enabling extension...`, identifier.id); - await this.extensionEnablementService.disableExtension(identifier); - this.logService.info(`Enabled extension`, identifier.id); - } - } - - for (const extensionToSync of installedExtensionsToSync) { - if (extensionToSync.state) { - const extensionState = JSON.parse(this.storageService.get(extensionToSync.identifier.id, StorageScope.GLOBAL) || '{}'); - forEach(extensionToSync.state, ({ key, value }) => extensionState[key] = value); - this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE); - } - } - return newlyEnabledExtensions; } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 6d9d58d1b..310fea185 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -31,9 +31,10 @@ export function getDisallowedIgnoredSettings(): string[] { export function getDefaultIgnoredSettings(): string[] { const allSettings = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); + const ignoreSyncSettings = Object.keys(allSettings).filter(setting => !!allSettings[setting].ignoreSync); const machineSettings = Object.keys(allSettings).filter(setting => allSettings[setting].scope === ConfigurationScope.MACHINE || allSettings[setting].scope === ConfigurationScope.MACHINE_OVERRIDABLE); const disallowedSettings = getDisallowedIgnoredSettings(); - return distinct([CONFIGURATION_SYNC_STORE_KEY, ...machineSettings, ...disallowedSettings]); + return distinct([CONFIGURATION_SYNC_STORE_KEY, ...ignoreSyncSettings, ...machineSettings, ...disallowedSettings]); } export function registerConfiguration(): IDisposable { @@ -52,10 +53,6 @@ export function registerConfiguration(): IDisposable { scope: ConfigurationScope.APPLICATION, tags: ['sync', 'usesOnlineServices'] }, - 'sync.keybindingsPerPlatform': { - type: 'boolean', - deprecationMessage: localize('sync.keybindingsPerPlatform.deprecated', "Deprecated, use settingsSync.keybindingsPerPlatform instead"), - }, 'settingsSync.ignoredExtensions': { 'type': 'array', markdownDescription: localize('settingsSync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always `${publisher}.${name}`. For example: `vscode.csharp`."), @@ -70,10 +67,6 @@ export function registerConfiguration(): IDisposable { disallowSyncIgnore: true, tags: ['sync', 'usesOnlineServices'] }, - 'sync.ignoredExtensions': { - 'type': 'array', - deprecationMessage: localize('sync.ignoredExtensions.deprecated', "Deprecated, use settingsSync.ignoredExtensions instead"), - }, 'settingsSync.ignoredSettings': { 'type': 'array', description: localize('settingsSync.ignoredSettings', "Configure settings to be ignored while synchronizing."), @@ -84,10 +77,6 @@ export function registerConfiguration(): IDisposable { uniqueItems: true, disallowSyncIgnore: true, tags: ['sync', 'usesOnlineServices'] - }, - 'sync.ignoredSettings': { - 'type': 'array', - deprecationMessage: localize('sync.ignoredSettings.deprecated', "Deprecated, use settingsSync.ignoredSettings instead"), } } }); diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 2cf4ac375..ad5494bc4 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -445,131 +445,171 @@ class ManualSyncTask extends Disposable implements IManualSyncTask { } async preview(): Promise<[SyncResource, ISyncResourcePreview][]> { - if (this.isDisposed) { - throw new Error('Disposed'); + try { + if (this.isDisposed) { + throw new Error('Disposed'); + } + if (!this.previewsPromise) { + this.previewsPromise = createCancelablePromise(token => this.getPreviews(token)); + } + if (!this.previews) { + this.previews = await this.previewsPromise; + } + return this.previews; + } catch (error) { + this.logService.error(error); + throw error; } - if (!this.previewsPromise) { - this.previewsPromise = createCancelablePromise(token => this.getPreviews(token)); - } - if (!this.previews) { - this.previews = await this.previewsPromise; - } - return this.previews; } async accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]> { - return this.performAction(resource, sychronizer => sychronizer.accept(resource, content)); + try { + return await this.performAction(resource, sychronizer => sychronizer.accept(resource, content)); + } catch (error) { + this.logService.error(error); + throw error; + } } async merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> { - if (resource) { - return this.performAction(resource, sychronizer => sychronizer.merge(resource)); - } else { - return this.mergeAll(); + try { + if (resource) { + return await this.performAction(resource, sychronizer => sychronizer.merge(resource)); + } else { + return await this.mergeAll(); + } + } catch (error) { + this.logService.error(error); + throw error; } } async discard(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> { - return this.performAction(resource, sychronizer => sychronizer.discard(resource)); + try { + return await this.performAction(resource, sychronizer => sychronizer.discard(resource)); + } catch (error) { + this.logService.error(error); + throw error; + } } async discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]> { - if (!this.previews) { - throw new Error('Missing preview. Create preview and try again.'); - } - if (this.synchronizingResources.length) { - throw new Error('Cannot discard while synchronizing resources'); - } + try { + if (!this.previews) { + throw new Error('Missing preview. Create preview and try again.'); + } + if (this.synchronizingResources.length) { + throw new Error('Cannot discard while synchronizing resources'); + } - const conflictResources: URI[] = []; - for (const [, syncResourcePreview] of this.previews) { - for (const resourcePreview of syncResourcePreview.resourcePreviews) { - if (resourcePreview.mergeState === MergeState.Conflict) { - conflictResources.push(resourcePreview.previewResource); + const conflictResources: URI[] = []; + for (const [, syncResourcePreview] of this.previews) { + for (const resourcePreview of syncResourcePreview.resourcePreviews) { + if (resourcePreview.mergeState === MergeState.Conflict) { + conflictResources.push(resourcePreview.previewResource); + } } } - } - for (const resource of conflictResources) { - await this.discard(resource); + for (const resource of conflictResources) { + await this.discard(resource); + } + return this.previews; + } catch (error) { + this.logService.error(error); + throw error; } - return this.previews; } async apply(): Promise<[SyncResource, ISyncResourcePreview][]> { - if (!this.previews) { - throw new Error('You need to create preview before applying'); - } - if (this.synchronizingResources.length) { - throw new Error('Cannot pull while synchronizing resources'); - } - const previews: [SyncResource, ISyncResourcePreview][] = []; - for (const [syncResource, preview] of this.previews) { - this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); - this._onSynchronizeResources.fire(this.synchronizingResources); + try { + if (!this.previews) { + throw new Error('You need to create preview before applying'); + } + if (this.synchronizingResources.length) { + throw new Error('Cannot pull while synchronizing resources'); + } + const previews: [SyncResource, ISyncResourcePreview][] = []; + for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); - const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; + const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; - /* merge those which are not yet merged */ - for (const resourcePreview of preview.resourcePreviews) { - if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) { - await synchroniser.merge(resourcePreview.previewResource); + /* merge those which are not yet merged */ + for (const resourcePreview of preview.resourcePreviews) { + if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) { + await synchroniser.merge(resourcePreview.previewResource); + } } - } - /* apply */ - const newPreview = await synchroniser.apply(false, this.syncHeaders); - if (newPreview) { - previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview)); - } + /* apply */ + const newPreview = await synchroniser.apply(false, this.syncHeaders); + if (newPreview) { + previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview)); + } - this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); - this._onSynchronizeResources.fire(this.synchronizingResources); + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + this.previews = previews; + return this.previews; + } catch (error) { + this.logService.error(error); + throw error; } - this.previews = previews; - return this.previews; } async pull(): Promise { - if (!this.previews) { - throw new Error('You need to create preview before applying'); - } - if (this.synchronizingResources.length) { - throw new Error('Cannot pull while synchronizing resources'); - } - for (const [syncResource, preview] of this.previews) { - this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); - this._onSynchronizeResources.fire(this.synchronizingResources); - const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; - for (const resourcePreview of preview.resourcePreviews) { - await synchroniser.accept(resourcePreview.remoteResource); + try { + if (!this.previews) { + throw new Error('You need to create preview before applying'); } - await synchroniser.apply(true, this.syncHeaders); - this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); - this._onSynchronizeResources.fire(this.synchronizingResources); + if (this.synchronizingResources.length) { + throw new Error('Cannot pull while synchronizing resources'); + } + for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); + const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; + for (const resourcePreview of preview.resourcePreviews) { + await synchroniser.accept(resourcePreview.remoteResource); + } + await synchroniser.apply(true, this.syncHeaders); + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + this.previews = []; + } catch (error) { + this.logService.error(error); + throw error; } - this.previews = []; } async push(): Promise { - if (!this.previews) { - throw new Error('You need to create preview before applying'); - } - if (this.synchronizingResources.length) { - throw new Error('Cannot pull while synchronizing resources'); - } - for (const [syncResource, preview] of this.previews) { - this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); - this._onSynchronizeResources.fire(this.synchronizingResources); - const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; - for (const resourcePreview of preview.resourcePreviews) { - await synchroniser.accept(resourcePreview.localResource); + try { + if (!this.previews) { + throw new Error('You need to create preview before applying'); } - await synchroniser.apply(true, this.syncHeaders); - this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); - this._onSynchronizeResources.fire(this.synchronizingResources); + if (this.synchronizingResources.length) { + throw new Error('Cannot pull while synchronizing resources'); + } + for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); + const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; + for (const resourcePreview of preview.resourcePreviews) { + await synchroniser.accept(resourcePreview.localResource); + } + await synchroniser.apply(true, this.syncHeaders); + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + this.previews = []; + } catch (error) { + this.logService.error(error); + throw error; } - this.previews = []; } async stop(): Promise { diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index 409586895..a4bde40f3 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable, } from 'vs/base/common/lifecycle'; import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID, CONFIGURATION_SYNC_STORE_KEY, IAuthenticationProvider, IUserDataSyncStoreManagementService, UserDataSyncStoreType, IUserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSync'; import { IRequestService, asText, isSuccess as isSuccessContext, asJson } from 'vs/platform/request/common/request'; import { joinPath, relativePath } from 'vs/base/common/resources'; @@ -180,6 +180,12 @@ export class UserDataSyncStoreClient extends Disposable implements IUserDataSync /* A requests session that limits requests per sessions */ this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService, this.logService); this.initDonotMakeRequestsUntil(); + this._register(toDisposable(() => { + if (this.resetDonotMakeRequestsUntilPromise) { + this.resetDonotMakeRequestsUntilPromise.cancel(); + this.resetDonotMakeRequestsUntilPromise = undefined; + } + })); } setAuthToken(token: string, type: string): void { @@ -210,6 +216,7 @@ export class UserDataSyncStoreClient extends Disposable implements IUserDataSync if (this._donotMakeRequestsUntil) { this.storageService.store(DONOT_MAKE_REQUESTS_UNTIL_KEY, this._donotMakeRequestsUntil.getTime(), StorageScope.GLOBAL, StorageTarget.MACHINE); this.resetDonotMakeRequestsUntilPromise = createCancelablePromise(token => timeout(this._donotMakeRequestsUntil!.getTime() - Date.now(), token).then(() => this.setDonotMakeRequestsUntil(undefined))); + this.resetDonotMakeRequestsUntilPromise.then(null, e => null /* ignore error */); } else { this.storageService.remove(DONOT_MAKE_REQUESTS_UNTIL_KEY, StorageScope.GLOBAL); } diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 5d2731d5e..247391119 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -181,7 +181,7 @@ suite('TestSynchronizer - Auto Sync', () => { teardown(() => disposableStore.clear()); test('status is syncing', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); @@ -198,7 +198,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('status is set correctly when sync is finished', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); const actual: SyncStatus[] = []; @@ -210,7 +210,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('status is set correctly when sync has errors', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasError: true, hasConflicts: false }; testObject.syncBarrier.open(); @@ -227,7 +227,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('status is set to hasConflicts when asked to sync if there are conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -238,7 +238,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('sync should not run if syncing already', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); const promise = Event.toPromise(testObject.onDoSyncCall.event); testObject.sync(await client.manifest()); @@ -255,7 +255,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('sync should not run if disabled', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); client.instantiationService.get(IUserDataSyncResourceEnablementService).setResourceEnablement(testObject.resource, false); const actual: SyncStatus[] = []; @@ -268,7 +268,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('sync should not run if there are conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -282,7 +282,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept preview during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -300,7 +300,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept remote during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -323,7 +323,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept local during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -345,7 +345,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept new content during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -368,7 +368,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept delete during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -390,7 +390,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept deleted local during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -411,7 +411,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept deleted remote during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); const fileService = client.instantiationService.get(IFileService); await fileService.writeFile(testObject.localResource, VSBuffer.fromString('some content')); @@ -431,7 +431,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('request latest data on precondition failure', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); // Sync once testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -458,7 +458,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('no requests are made to server when local change is triggered', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -471,7 +471,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('status is reset when getting latest remote data fails', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.failWhenGettingLatestRemoteUserData = true; try { @@ -502,7 +502,7 @@ suite('TestSynchronizer - Manual Sync', () => { teardown(() => disposableStore.clear()); test('preview', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -514,7 +514,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -528,7 +528,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -542,7 +542,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> merge -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -557,7 +557,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> merge -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -577,7 +577,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -597,7 +597,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> merge -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -617,7 +617,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -630,7 +630,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -650,7 +650,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> merge -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -665,7 +665,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> merge -> discard -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -681,7 +681,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -696,7 +696,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -712,7 +712,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -728,7 +728,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> merge -> accept -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -744,7 +744,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> merge -> discard -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -764,7 +764,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -785,7 +785,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard -> merge -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -808,7 +808,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -820,7 +820,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -834,7 +834,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> merge -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -849,7 +849,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -864,7 +864,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> merge -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -887,7 +887,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -901,7 +901,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -923,7 +923,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -938,7 +938,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> discard -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -954,7 +954,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> accept -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -969,7 +969,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> accept -> discard -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -985,7 +985,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> accept -> discard -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -1001,7 +1001,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> discard -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -1017,7 +1017,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> accept -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -1033,7 +1033,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> discard -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -1053,7 +1053,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> accept -> discard -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index 49df9ea7e..a12085fe7 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -40,7 +40,7 @@ suite('UserDataAutoSyncService', () => { await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run(); target.reset(); - const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Trigger auto sync with settings change await testObject.triggerSync([SyncResource.Settings], false, false); @@ -62,7 +62,7 @@ suite('UserDataAutoSyncService', () => { await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run(); target.reset(); - const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Trigger auto sync with settings change multiple times for (let counter = 0; counter < 2; counter++) { @@ -88,7 +88,7 @@ suite('UserDataAutoSyncService', () => { await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run(); target.reset(); - const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Trigger auto sync with window focus once await testObject.triggerSync(['windowFocus'], true, false); @@ -110,7 +110,7 @@ suite('UserDataAutoSyncService', () => { await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run(); target.reset(); - const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Trigger auto sync with window focus multiple times for (let counter = 0; counter < 2; counter++) { @@ -129,7 +129,7 @@ suite('UserDataAutoSyncService', () => { const target = new UserDataSyncTestServer(); const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); @@ -165,7 +165,7 @@ suite('UserDataAutoSyncService', () => { const target = new UserDataSyncTestServer(); const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Sync once and reset requests await testObject.sync(); @@ -185,7 +185,7 @@ suite('UserDataAutoSyncService', () => { const target = new UserDataSyncTestServer(); const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Sync once and reset requests await testObject.sync(); @@ -220,7 +220,7 @@ suite('UserDataAutoSyncService', () => { const target = new UserDataSyncTestServer(); const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Sync once and reset requests await testObject.sync(); @@ -250,7 +250,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); // Reset from the first client @@ -279,7 +279,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); // Disable current machine @@ -311,7 +311,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); // Remove current machine @@ -339,7 +339,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); // Reset from the first client @@ -371,7 +371,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); const errorPromise = Event.toPromise(testObject.onError); while (target.requests.length < 5) { @@ -389,7 +389,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); while (target.requests.length < 5) { await testObject.sync(); @@ -407,7 +407,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.triggerSync(['some reason'], true, true); assert.equal(target.requestsWithAllHeaders[0].headers!['Cache-Control'], 'no-cache'); @@ -419,7 +419,7 @@ suite('UserDataAutoSyncService', () => { // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.triggerSync(['some reason'], true, false); assert.equal(target.requestsWithAllHeaders[0].headers!['Cache-Control'], undefined); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 322046976..b9d4381d7 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -83,9 +83,9 @@ export class UserDataSyncClient extends Disposable { fileService.registerProvider(Schemas.inMemory, new InMemoryFileSystemProvider()); this.instantiationService.stub(IFileService, fileService); - this.instantiationService.stub(IStorageService, new InMemoryStorageService()); + this.instantiationService.stub(IStorageService, this._register(new InMemoryStorageService())); - const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); await configurationService.initialize(); this.instantiationService.stub(IConfigurationService, configurationService); @@ -93,20 +93,20 @@ export class UserDataSyncClient extends Disposable { this.instantiationService.stub(IUserDataSyncLogService, logService); this.instantiationService.stub(ITelemetryService, NullTelemetryService); - this.instantiationService.stub(IUserDataSyncStoreManagementService, this.instantiationService.createInstance(UserDataSyncStoreManagementService)); - this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService)); + this.instantiationService.stub(IUserDataSyncStoreManagementService, this._register(this.instantiationService.createInstance(UserDataSyncStoreManagementService))); + this.instantiationService.stub(IUserDataSyncStoreService, this._register(this.instantiationService.createInstance(UserDataSyncStoreService))); - const userDataSyncAccountService: IUserDataSyncAccountService = this.instantiationService.createInstance(UserDataSyncAccountService); + const userDataSyncAccountService: IUserDataSyncAccountService = this._register(this.instantiationService.createInstance(UserDataSyncAccountService)); await userDataSyncAccountService.updateAccount({ authenticationProviderId: 'authenticationProviderId', token: 'token' }); this.instantiationService.stub(IUserDataSyncAccountService, userDataSyncAccountService); - this.instantiationService.stub(IUserDataSyncMachinesService, this.instantiationService.createInstance(UserDataSyncMachinesService)); - this.instantiationService.stub(IUserDataSyncBackupStoreService, this.instantiationService.createInstance(UserDataSyncBackupStoreService)); + this.instantiationService.stub(IUserDataSyncMachinesService, this._register(this.instantiationService.createInstance(UserDataSyncMachinesService))); + this.instantiationService.stub(IUserDataSyncBackupStoreService, this._register(this.instantiationService.createInstance(UserDataSyncBackupStoreService))); this.instantiationService.stub(IUserDataSyncUtilService, new TestUserDataSyncUtilService()); - this.instantiationService.stub(IUserDataSyncResourceEnablementService, this.instantiationService.createInstance(UserDataSyncResourceEnablementService)); + this.instantiationService.stub(IUserDataSyncResourceEnablementService, this._register(this.instantiationService.createInstance(UserDataSyncResourceEnablementService))); - this.instantiationService.stub(IGlobalExtensionEnablementService, this.instantiationService.createInstance(GlobalExtensionEnablementService)); - this.instantiationService.stub(IExtensionsStorageSyncService, this.instantiationService.createInstance(ExtensionsStorageSyncService)); + this.instantiationService.stub(IGlobalExtensionEnablementService, this._register(this.instantiationService.createInstance(GlobalExtensionEnablementService))); + this.instantiationService.stub(IExtensionsStorageSyncService, this._register(this.instantiationService.createInstance(ExtensionsStorageSyncService))); this.instantiationService.stub(IIgnoredExtensionsManagementService, this.instantiationService.createInstance(IgnoredExtensionsManagementService)); this.instantiationService.stub(IExtensionManagementService, >{ async getInstalled() { return []; }, @@ -118,8 +118,8 @@ export class UserDataSyncClient extends Disposable { async getCompatibleExtension() { return null; } }); - this.instantiationService.stub(IUserDataAutoSyncEnablementService, this.instantiationService.createInstance(UserDataAutoSyncEnablementService)); - this.instantiationService.stub(IUserDataSyncService, this.instantiationService.createInstance(UserDataSyncService)); + this.instantiationService.stub(IUserDataAutoSyncEnablementService, this._register(this.instantiationService.createInstance(UserDataAutoSyncEnablementService))); + this.instantiationService.stub(IUserDataSyncService, this._register(this.instantiationService.createInstance(UserDataSyncService))); if (!empty) { await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({}))); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts index 2c7d1734d..4550c939b 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts @@ -58,7 +58,7 @@ suite('UserDataSyncStoreManagementService', () => { authenticationProviders: [{ id: 'configuredAuthProvider', scopes: [] }] }; - const testObject: IUserDataSyncStoreManagementService = client.instantiationService.createInstance(UserDataSyncStoreManagementService); + const testObject: IUserDataSyncStoreManagementService = disposableStore.add(client.instantiationService.createInstance(UserDataSyncStoreManagementService)); assert.equal(testObject.userDataSyncStore?.url.toString(), expected.url.toString()); assert.equal(testObject.userDataSyncStore?.defaultUrl.toString(), expected.defaultUrl.toString()); @@ -419,7 +419,7 @@ suite('UserDataSyncStoreService', () => { await testObject.manifest(); } catch (e) { } - const target = client.instantiationService.createInstance(UserDataSyncStoreService); + const target = disposableStore.add(client.instantiationService.createInstance(UserDataSyncStoreService)); assert.equal(target.donotMakeRequestsUntil?.getTime(), testObject.donotMakeRequestsUntil?.getTime()); }); @@ -434,7 +434,7 @@ suite('UserDataSyncStoreService', () => { } catch (e) { } await timeout(300); - const target = client.instantiationService.createInstance(UserDataSyncStoreService); + const target = disposableStore.add(client.instantiationService.createInstance(UserDataSyncStoreService)); assert.ok(!target.donotMakeRequestsUntil); }); diff --git a/src/vs/platform/webview/common/resourceLoader.ts b/src/vs/platform/webview/common/resourceLoader.ts index ee64c8ef4..46f0217fa 100644 --- a/src/vs/platform/webview/common/resourceLoader.ts +++ b/src/vs/platform/webview/common/resourceLoader.ts @@ -9,6 +9,7 @@ import { isUNC } from 'vs/base/common/extpath'; import { Schemas } from 'vs/base/common/network'; import { sep } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; +import { ILogService } from 'vs/platform/log/common/log'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRequestService } from 'vs/platform/request/common/request'; import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes'; @@ -24,7 +25,8 @@ export namespace WebviewResourceResponse { constructor( public readonly stream: VSBufferReadableStream, - public readonly mimeType: string + public readonly etag: string | undefined, + public readonly mimeType: string, ) { } } @@ -35,7 +37,7 @@ export namespace WebviewResourceResponse { } interface FileReader { - readFileStream(resource: URI): Promise; + readFileStream(resource: URI): Promise<{ stream: VSBufferReadableStream, etag?: string }>; } export async function loadLocalResource( @@ -48,8 +50,14 @@ export async function loadLocalResource( }, fileReader: FileReader, requestService: IRequestService, + logService: ILogService, ): Promise { + logService.debug(`loadLocalResource - being. requestUri=${requestUri}`); + let resourceToLoad = getResourceToLoad(requestUri, options.roots); + + logService.debug(`loadLocalResource - found resource to load. requestUri=${requestUri}, resourceToLoad=${resourceToLoad}`); + if (!resourceToLoad) { return WebviewResourceResponse.AccessDenied; } @@ -63,17 +71,23 @@ export async function loadLocalResource( if (resourceToLoad.scheme === Schemas.http || resourceToLoad.scheme === Schemas.https) { const response = await requestService.request({ url: resourceToLoad.toString(true) }, CancellationToken.None); + logService.debug(`loadLocalResource - Loaded over http(s). requestUri=${requestUri}, response=${response.res.statusCode}`); + if (response.res.statusCode === 200) { - return new WebviewResourceResponse.StreamSuccess(response.stream, mime); + return new WebviewResourceResponse.StreamSuccess(response.stream, undefined, mime); } return WebviewResourceResponse.Failed; } try { const contents = await fileReader.readFileStream(resourceToLoad); - return new WebviewResourceResponse.StreamSuccess(contents, mime); + logService.debug(`loadLocalResource - Loaded using fileReader. requestUri=${requestUri}`); + + return new WebviewResourceResponse.StreamSuccess(contents.stream, contents.etag, mime); } catch (err) { + logService.debug(`loadLocalResource - Error using fileReader. requestUri=${requestUri}`); console.log(err); + return WebviewResourceResponse.Failed; } } diff --git a/src/vs/platform/webview/common/webviewPortMapping.ts b/src/vs/platform/webview/common/webviewPortMapping.ts index 581543130..4e9c22976 100644 --- a/src/vs/platform/webview/common/webviewPortMapping.ts +++ b/src/vs/platform/webview/common/webviewPortMapping.ts @@ -19,7 +19,7 @@ export interface IWebviewPortMapping { */ export class WebviewPortMappingManager implements IDisposable { - private readonly _tunnels = new Map>(); + private readonly _tunnels = new Map(); constructor( private readonly _getExtensionLocation: () => URI | undefined, @@ -60,19 +60,19 @@ export class WebviewPortMappingManager implements IDisposable { return undefined; } - dispose() { + async dispose() { for (const tunnel of this._tunnels.values()) { - tunnel.then(tunnel => tunnel.dispose()); + await tunnel.dispose(); } this._tunnels.clear(); } - private getOrCreateTunnel(remoteAuthority: IAddress, remotePort: number): Promise | undefined { + private async getOrCreateTunnel(remoteAuthority: IAddress, remotePort: number): Promise { const existing = this._tunnels.get(remotePort); if (existing) { return existing; } - const tunnel = this.tunnelService.openTunnel({ getAddress: async () => remoteAuthority }, undefined, remotePort); + const tunnel = await this.tunnelService.openTunnel({ getAddress: async () => remoteAuthority }, undefined, remotePort); if (tunnel) { this._tunnels.set(remotePort, tunnel); } diff --git a/src/vs/platform/webview/electron-main/webviewMainService.ts b/src/vs/platform/webview/electron-main/webviewMainService.ts index 03a6e306c..92bd47e2d 100644 --- a/src/vs/platform/webview/electron-main/webviewMainService.ts +++ b/src/vs/platform/webview/electron-main/webviewMainService.ts @@ -8,6 +8,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { IRequestService } from 'vs/platform/request/common/request'; import { IWebviewManagerService, RegisterWebviewMetadata, WebviewWebContentsId, WebviewWindowId } from 'vs/platform/webview/common/webviewManagerService'; @@ -24,12 +25,13 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer constructor( @IFileService fileService: IFileService, + @ILogService logService: ILogService, @IRequestService requestService: IRequestService, @ITunnelService tunnelService: ITunnelService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, ) { super(); - this.protocolProvider = this._register(new WebviewProtocolProvider(fileService, requestService, windowsMainService)); + this.protocolProvider = this._register(new WebviewProtocolProvider(fileService, logService, requestService, windowsMainService)); this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService)); } diff --git a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts index a019d267e..a8c2e0fef 100644 --- a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts +++ b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts @@ -10,6 +10,7 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRequestService } from 'vs/platform/request/common/request'; import { loadLocalResource, webviewPartitionId, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader'; @@ -38,8 +39,9 @@ export class WebviewProtocolProvider extends Disposable { constructor( @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService, @IRequestService private readonly requestService: IRequestService, - @IWindowsMainService readonly windowsMainService: IWindowsMainService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, ) { super(); @@ -125,7 +127,10 @@ export class WebviewProtocolProvider extends Disposable { } } - private async handleWebviewRequest(request: Electron.Request, callback: any) { + private async handleWebviewRequest( + request: Electron.ProtocolRequest, + callback: (response: string | Electron.ProtocolResponse) => void + ) { try { const uri = URI.parse(request.url); const entry = WebviewProtocolProvider.validWebviewFilePaths.get(uri.path); @@ -144,8 +149,8 @@ export class WebviewProtocolProvider extends Disposable { } private async handleWebviewResourceRequest( - request: Electron.Request, - callback: (stream?: NodeJS.ReadableStream | Electron.StreamProtocolResponse | undefined) => void + request: Electron.ProtocolRequest, + callback: (stream: NodeJS.ReadableStream | Electron.ProtocolResponse) => void ) { try { const uri = URI.parse(request.url); @@ -170,10 +175,14 @@ export class WebviewProtocolProvider extends Disposable { }; } - const fileService = { - readFileStream: async (resource: URI): Promise => { + const fileReader = { + readFileStream: async (resource: URI): Promise<{ stream: VSBufferReadableStream, etag?: string }> => { if (resource.scheme === Schemas.file) { - return (await this.fileService.readFileStream(resource)).value; + const result = (await this.fileService.readFileStream(resource)); + return { + stream: result.value, + etag: result.etag + }; } // Unknown uri scheme. Try delegating the file read back to the renderer @@ -196,7 +205,7 @@ export class WebviewProtocolProvider extends Disposable { throw new FileOperationError('Could not read file', FileOperationResult.FILE_NOT_FOUND); } - return bufferToStream(result); + return { stream: bufferToStream(result), etag: undefined }; } }; @@ -205,29 +214,55 @@ export class WebviewProtocolProvider extends Disposable { roots: metadata.localResourceRoots, remoteConnectionData: metadata.remoteConnectionData, rewriteUri, - }, fileService, this.requestService); + }, fileReader, this.requestService, this.logService); if (result.type === WebviewResourceResponse.Type.Success) { + const cacheHeaders: Record = result.etag ? { + 'ETag': result.etag, + 'Cache-Control': 'no-cache' + } : {}; + + const ifNoneMatch = request.headers['If-None-Match']; + if (ifNoneMatch && result.etag === ifNoneMatch) { + /* + * Note that the server generating a 304 response MUST + * generate any of the following header fields that would + * have been sent in a 200 (OK) response to the same request: + * Cache-Control, Content-Location, Date, ETag, Expires, and Vary. + * (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) + */ + return callback({ + statusCode: 304, // not modified + data: undefined, // The request fails if `data` is not set + headers: { + 'Content-Type': result.mimeType, + 'Access-Control-Allow-Origin': '*', + ...cacheHeaders + } + }); + } + return callback({ statusCode: 200, data: this.streamToNodeReadable(result.stream), headers: { 'Content-Type': result.mimeType, 'Access-Control-Allow-Origin': '*', + ...cacheHeaders } }); } if (result.type === WebviewResourceResponse.Type.AccessDenied) { console.error('Webview: Cannot load resource outside of protocol root'); - return callback({ data: null, statusCode: 401 }); + return callback({ data: undefined, statusCode: 401 }); } } } catch { // noop } - return callback({ data: null, statusCode: 404 }); + return callback({ data: undefined, statusCode: 404 }); } public didLoadResource(requestId: number, content: VSBuffer | undefined) { diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 40c952136..f97741d19 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -9,7 +9,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { LogLevel } from 'vs/platform/log/common/log'; -import { ExportData } from 'vs/base/common/performance'; +import { PerformanceMark } from 'vs/base/common/performance'; export const WindowMinimumSize = { WIDTH: 400, @@ -18,38 +18,37 @@ export const WindowMinimumSize = { }; export interface IBaseOpenWindowsOptions { - forceReuseWindow?: boolean; + readonly forceReuseWindow?: boolean; } export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { - forceNewWindow?: boolean; - preferNewWindow?: boolean; + readonly forceNewWindow?: boolean; + readonly preferNewWindow?: boolean; - noRecentEntry?: boolean; + readonly noRecentEntry?: boolean; - addMode?: boolean; + readonly addMode?: boolean; - diffMode?: boolean; - gotoLineMode?: boolean; + readonly diffMode?: boolean; + readonly gotoLineMode?: boolean; - waitMarkerFileURI?: URI; + readonly waitMarkerFileURI?: URI; } export interface IAddFoldersRequest { - foldersToAdd: UriComponents[]; + readonly foldersToAdd: UriComponents[]; } export interface IOpenedWindow { - id: number; - workspace?: IWorkspaceIdentifier; - folderUri?: ISingleFolderWorkspaceIdentifier; - title: string; - filename?: string; - dirty: boolean; + readonly id: number; + readonly workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; + readonly title: string; + readonly filename?: string; + readonly dirty: boolean; } export interface IOpenEmptyWindowOptions extends IBaseOpenWindowsOptions { - remoteAuthority?: string; + readonly remoteAuthority?: string; } export type IWindowOpenable = IWorkspaceToOpen | IFolderToOpen | IFileToOpen; @@ -59,15 +58,15 @@ export interface IBaseWindowOpenable { } export interface IWorkspaceToOpen extends IBaseWindowOpenable { - workspaceUri: URI; + readonly workspaceUri: URI; } export interface IFolderToOpen extends IBaseWindowOpenable { - folderUri: URI; + readonly folderUri: URI; } export interface IFileToOpen extends IBaseWindowOpenable { - fileUri: URI; + readonly fileUri: URI; } export function isWorkspaceToOpen(uriToOpen: IWindowOpenable): uriToOpen is IWorkspaceToOpen { @@ -96,26 +95,25 @@ export function getMenuBarVisibility(configurationService: IConfigurationService } export interface IWindowsConfiguration { - window: IWindowSettings; + readonly window: IWindowSettings; } export interface IWindowSettings { - openFilesInNewWindow: 'on' | 'off' | 'default'; - openFoldersInNewWindow: 'on' | 'off' | 'default'; - openWithoutArgumentsInNewWindow: 'on' | 'off'; - restoreWindows: 'preserve' | 'all' | 'folders' | 'one' | 'none'; - restoreFullscreen: boolean; - zoomLevel: number; - titleBarStyle: 'native' | 'custom'; - autoDetectHighContrast: boolean; - menuBarVisibility: MenuBarVisibility; - newWindowDimensions: 'default' | 'inherit' | 'offset' | 'maximized' | 'fullscreen'; - nativeTabs: boolean; - nativeFullScreen: boolean; - enableMenuBarMnemonics: boolean; - closeWhenEmpty: boolean; - clickThroughInactive: boolean; - enableExperimentalProxyLoginDialog: boolean; + readonly openFilesInNewWindow: 'on' | 'off' | 'default'; + readonly openFoldersInNewWindow: 'on' | 'off' | 'default'; + readonly openWithoutArgumentsInNewWindow: 'on' | 'off'; + readonly restoreWindows: 'preserve' | 'all' | 'folders' | 'one' | 'none'; + readonly restoreFullscreen: boolean; + readonly zoomLevel: number; + readonly titleBarStyle: 'native' | 'custom'; + readonly autoDetectHighContrast: boolean; + readonly menuBarVisibility: MenuBarVisibility; + readonly newWindowDimensions: 'default' | 'inherit' | 'offset' | 'maximized' | 'fullscreen'; + readonly nativeTabs: boolean; + readonly nativeFullScreen: boolean; + readonly enableMenuBarMnemonics: boolean; + readonly closeWhenEmpty: boolean; + readonly clickThroughInactive: boolean; } export function getTitleBarStyle(configurationService: IConfigurationService): 'native' | 'custom' { @@ -154,24 +152,24 @@ export interface IPath extends IPathData { export interface IPathData { // the file path to open within the instance - fileUri?: UriComponents; + readonly fileUri?: UriComponents; // the line number in the file path to open - lineNumber?: number; + readonly lineNumber?: number; // the column number in the file path to open - columnNumber?: number; + readonly columnNumber?: number; // a hint that the file exists. if true, the // file exists, if false it does not. with // undefined the state is unknown. - exists?: boolean; + readonly exists?: boolean; // Specifies if the file should be only be opened if it exists - openOnlyIfExists?: boolean; + readonly openOnlyIfExists?: boolean; // Specifies an optional id to override the editor used to edit the resource, e.g. custom editor. - overrideId?: string; + readonly overrideId?: string; } export interface IPathsToWaitFor extends IPathsToWaitForData { @@ -180,36 +178,36 @@ export interface IPathsToWaitFor extends IPathsToWaitForData { } interface IPathsToWaitForData { - paths: IPathData[]; - waitMarkerFileUri: UriComponents; + readonly paths: IPathData[]; + readonly waitMarkerFileUri: UriComponents; } export interface IOpenFileRequest { - filesToOpenOrCreate?: IPathData[]; - filesToDiff?: IPathData[]; + readonly filesToOpenOrCreate?: IPathData[]; + readonly filesToDiff?: IPathData[]; } /** * Additional context for the request on native only. */ export interface INativeOpenFileRequest extends IOpenFileRequest { - termProgram?: string; - filesToWait?: IPathsToWaitForData; + readonly termProgram?: string; + readonly filesToWait?: IPathsToWaitForData; } export interface INativeRunActionInWindowRequest { - id: string; - from: 'menu' | 'touchbar' | 'mouse'; - args?: any[]; + readonly id: string; + readonly from: 'menu' | 'touchbar' | 'mouse'; + readonly args?: any[]; } export interface INativeRunKeybindingInWindowRequest { - userSettingsLabel: string; + readonly userSettingsLabel: string; } export interface IColorScheme { - dark: boolean; - highContrast: boolean; + readonly dark: boolean; + readonly highContrast: boolean; } export interface IWindowConfiguration { @@ -225,7 +223,7 @@ export interface IWindowConfiguration { } export interface IOSConfiguration { - release: string; + readonly release: string; } export interface INativeWindowConfiguration extends IWindowConfiguration, NativeParsedArgs { @@ -241,8 +239,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native nodeCachedDataDir?: string; partsSplashPath: string; - workspace?: IWorkspaceIdentifier; - folderUri?: ISingleFolderWorkspaceIdentifier; + workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; isInitialStartup?: boolean; logLevel: LogLevel; @@ -250,7 +247,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native fullscreen?: boolean; maximized?: boolean; accessibilitySupport?: boolean; - perfEntries: ExportData; + perfMarks: PerformanceMark[]; userEnv: IProcessEnvironment; filesToWait?: IPathsToWaitFor; diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 83eccca73..8bd104650 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -4,18 +4,38 @@ *--------------------------------------------------------------------------------------------*/ import { IWindowOpenable, IOpenEmptyWindowOptions, INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProcessEnvironment } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { URI } from 'vs/base/common/uri'; import { Rectangle, BrowserWindow, WebContents } from 'electron'; import { IDisposable } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; +export const enum OpenContext { + + // opening when running from the command line + CLI, + + // macOS only: opening from the dock (also when opening files to a running instance from desktop) + DOCK, + + // opening from the main application window + MENU, + + // opening from a file or folder dialog + DIALOG, + + // opening from the OS's UI + DESKTOP, + + // opening through the API + API +} + export interface IWindowState { width?: number; height?: number; @@ -45,8 +65,8 @@ export interface ICodeWindow extends IDisposable { readonly win: BrowserWindow; readonly config: INativeWindowConfiguration | undefined; - readonly openedFolderUri?: URI; - readonly openedWorkspace?: IWorkspaceIdentifier; + readonly openedWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; + readonly backupPath?: string; readonly remoteAuthority?: string; @@ -64,7 +84,7 @@ export interface ICodeWindow extends IDisposable { addTabbedWindow(window: ICodeWindow): void; - load(config: INativeWindowConfiguration, isReload?: boolean): void; + load(config: INativeWindowConfiguration, options?: { isReload?: boolean }): void; reload(cli?: NativeParsedArgs): void; focus(options?: { force: boolean }): void; @@ -104,9 +124,11 @@ export interface IWindowsMainService { readonly _serviceBrand: undefined; + readonly onWindowsCountChanged: Event; + readonly onWindowOpened: Event; readonly onWindowReady: Event; - readonly onWindowsCountChanged: Event; + readonly onWindowDestroyed: Event; open(openConfig: IOpenConfiguration): ICodeWindow[]; openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[]; @@ -115,13 +137,14 @@ export interface IWindowsMainService { sendToFocused(channel: string, ...args: any[]): void; sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void; + getWindows(): ICodeWindow[]; + getWindowCount(): number; + getFocusedWindow(): ICodeWindow | undefined; getLastActiveWindow(): ICodeWindow | undefined; getWindowById(windowId: number): ICodeWindow | undefined; getWindowByWebContents(webContents: WebContents): ICodeWindow | undefined; - getWindows(): ICodeWindow[]; - getWindowCount(): number; } export interface IBaseOpenConfiguration { diff --git a/src/vs/platform/windows/electron-main/windowsFinder.ts b/src/vs/platform/windows/electron-main/windowsFinder.ts new file mode 100644 index 000000000..30f760183 --- /dev/null +++ b/src/vs/platform/windows/electron-main/windowsFinder.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { IWorkspaceIdentifier, IResolvedWorkspace, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; + +export function findWindowOnFile(windows: ICodeWindow[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null): ICodeWindow | undefined { + + // First check for windows with workspaces that have a parent folder of the provided path opened + for (const window of windows) { + const workspace = window.openedWorkspace; + if (isWorkspaceIdentifier(workspace)) { + const resolvedWorkspace = localWorkspaceResolver(workspace); + + // resolved workspace: folders are known and can be compared with + if (resolvedWorkspace) { + if (resolvedWorkspace.folders.some(folder => extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, folder.uri))) { + return window; + } + } + + // unresolved: can only compare with workspace location + else { + if (extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, workspace.configPath)) { + return window; + } + } + } + } + + // Then go with single folder windows that are parent of the provided file path + const singleFolderWindowsOnFilePath = windows.filter(window => isSingleFolderWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, window.openedWorkspace.uri)); + if (singleFolderWindowsOnFilePath.length) { + return singleFolderWindowsOnFilePath.sort((windowA, windowB) => -((windowA.openedWorkspace as ISingleFolderWorkspaceIdentifier).uri.path.length - (windowB.openedWorkspace as ISingleFolderWorkspaceIdentifier).uri.path.length))[0]; + } + + return undefined; +} + +export function findWindowOnWorkspaceOrFolder(windows: ICodeWindow[], folderOrWorkspaceConfigUri: URI): ICodeWindow | undefined { + + for (const window of windows) { + + // check for workspace config path + if (isWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, folderOrWorkspaceConfigUri)) { + return window; + } + + // check for folder path + if (isSingleFolderWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.uri, folderOrWorkspaceConfigUri)) { + return window; + } + } + + return undefined; +} + + +export function findWindowOnExtensionDevelopmentPath(windows: ICodeWindow[], extensionDevelopmentPaths: string[]): ICodeWindow | undefined { + + const matches = (uriString: string): boolean => { + return extensionDevelopmentPaths.some(path => extUriBiasedIgnorePathCase.isEqual(URI.file(path), URI.file(uriString))); + }; + + for (const window of windows) { + + // match on extension development path. the path can be one or more paths + // so we check if any of the paths match on any of the provided ones + if (window.config?.extensionDevelopmentPath?.some(path => matches(path))) { + return window; + } + } + + return undefined; +} diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 6bcedb5c7..eebedf006 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { statSync, unlink } from 'fs'; +import { statSync } from 'fs'; import { basename, normalize, join, posix } from 'vs/base/common/path'; import { localize } from 'vs/nls'; import { coalesce, distinct, firstOrDefault } from 'vs/base/common/arrays'; @@ -12,166 +12,126 @@ import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { IStateService } from 'vs/platform/state/node/state'; -import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window'; -import { screen, BrowserWindow, MessageBoxOptions, Display, app, WebContents } from 'electron'; +import { CodeWindow } from 'vs/code/electron-main/window'; +import { BrowserWindow, MessageBoxOptions, WebContents } from 'electron'; import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; -import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest, IPathsToWaitFor, INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, OpenContext } from 'vs/platform/windows/node/window'; +import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest, IPathsToWaitFor, INativeWindowConfiguration, INativeOpenFileRequest } from 'vs/platform/windows/common/windows'; +import { findWindowOnFile, findWindowOnWorkspaceOrFolder, findWindowOnExtensionDevelopmentPath } from 'vs/platform/windows/electron-main/windowsFinder'; import { Emitter } from 'vs/base/common/event'; import product from 'vs/platform/product/common/product'; -import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode, IOpenEmptyConfiguration } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IOpenEmptyConfiguration, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; -import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension, IRecent } from 'vs/platform/workspaces/common/workspaces'; +import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; +import { IWorkspaceIdentifier, hasWorkspaceFileExtension, IRecent, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { normalizePath, originalFSPath, removeTrailingPathSeparator, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; -import { restoreWindowsState, WindowsStateStorageData, getWindowsStateStoreData } from 'vs/platform/windows/electron-main/windowsStateStorage'; -import { getWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { IWindowState, WindowsStateHandler } from 'vs/platform/windows/electron-main/windowsStateHandler'; +import { getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier, IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { once } from 'vs/base/common/functional'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { isWindowsDriveLetter, toSlashes, parseLineAndColumnAware } from 'vs/base/common/extpath'; +import { isWindowsDriveLetter, toSlashes, parseLineAndColumnAware, sanitizeFilePath } from 'vs/base/common/extpath'; import { CharCode } from 'vs/base/common/charCode'; import { getPathLabel } from 'vs/base/common/labels'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IFileService } from 'vs/platform/files/common/files'; -export interface IWindowState { - workspace?: IWorkspaceIdentifier; - folderUri?: URI; - backupPath?: string; - remoteAuthority?: string; - uiState: ISingleWindowState; -} - -export interface IWindowsState { - lastActiveWindow?: IWindowState; - lastPluginDevelopmentHostWindow?: IWindowState; - openedWindows: IWindowState[]; -} - -interface INewWindowState extends ISingleWindowState { - hasDefaultState?: boolean; -} +//#region Helper Interfaces type RestoreWindowsSetting = 'preserve' | 'all' | 'folders' | 'one' | 'none'; interface IOpenBrowserWindowOptions { - userEnv?: IProcessEnvironment; - cli?: NativeParsedArgs; + readonly userEnv?: IProcessEnvironment; + readonly cli?: NativeParsedArgs; - workspace?: IWorkspaceIdentifier; - folderUri?: URI; + readonly workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; - remoteAuthority?: string; + readonly remoteAuthority?: string; - initialStartup?: boolean; + readonly initialStartup?: boolean; - filesToOpen?: IFilesToOpen; + readonly filesToOpen?: IFilesToOpen; - forceNewWindow?: boolean; - forceNewTabbedWindow?: boolean; - windowToUse?: ICodeWindow; + readonly forceNewWindow?: boolean; + readonly forceNewTabbedWindow?: boolean; + readonly windowToUse?: ICodeWindow; - emptyWindowBackupInfo?: IEmptyWindowBackupInfo; + readonly emptyWindowBackupInfo?: IEmptyWindowBackupInfo; } -interface IPathParseOptions { - ignoreFileNotFound?: boolean; - gotoLineMode?: boolean; - remoteAuthority?: string; +interface IPathResolveOptions { + readonly ignoreFileNotFound?: boolean; + readonly gotoLineMode?: boolean; + readonly remoteAuthority?: string; } interface IFilesToOpen { + readonly remoteAuthority?: string; + filesToOpenOrCreate: IPath[]; filesToDiff: IPath[]; filesToWait?: IPathsToWaitFor; - remoteAuthority?: string; } interface IPathToOpen extends IPath { - // the workspace for a Code instance to open - workspace?: IWorkspaceIdentifier; + // the workspace to open + readonly workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; - // the folder path for a Code instance to open - folderUri?: URI; - - // the backup path for a Code instance to use - backupPath?: string; + // the backup path to use + readonly backupPath?: string; // the remote authority for the Code instance to open. Undefined if not remote. - remoteAuthority?: string; + readonly remoteAuthority?: string; // optional label for the recent history label?: string; } -function isFolderPathToOpen(path: IPathToOpen): path is IFolderPathToOpen { - return !!path.folderUri; +interface IWorkspacePathToOpen extends IPathToOpen { + readonly workspace: IWorkspaceIdentifier; } -interface IFolderPathToOpen { - - // the folder path for a Code instance to open - folderUri: URI; - - // the backup path for a Code instance to use - backupPath?: string; - - // the remote authority for the Code instance to open. Undefined if not remote. - remoteAuthority?: string; - - // optional label for the recent history - label?: string; +interface ISingleFolderWorkspacePathToOpen extends IPathToOpen { + readonly workspace: ISingleFolderWorkspaceIdentifier; } -function isWorkspacePathToOpen(path: IPathToOpen): path is IWorkspacePathToOpen { - return !!path.workspace; +function isWorkspacePathToOpen(path: IPathToOpen | undefined): path is IWorkspacePathToOpen { + return isWorkspaceIdentifier(path?.workspace); } -interface IWorkspacePathToOpen { - - // the workspace for a Code instance to open - workspace: IWorkspaceIdentifier; - - // the backup path for a Code instance to use - backupPath?: string; - - // the remote authority for the Code instance to open. Undefined if not remote. - remoteAuthority?: string; - - // optional label for the recent history - label?: string; +function isSingleFolderWorkspacePathToOpen(path: IPathToOpen | undefined): path is ISingleFolderWorkspacePathToOpen { + return isSingleFolderWorkspaceIdentifier(path?.workspace); } +//#endregion + export class WindowsMainService extends Disposable implements IWindowsMainService { declare readonly _serviceBrand: undefined; - private static readonly windowsStateStorageKey = 'windowsState'; - private static readonly WINDOWS: ICodeWindow[] = []; - private readonly windowsState: IWindowsState; - private lastClosedWindowState?: IWindowState; - - private shuttingDown = false; - private readonly _onWindowOpened = this._register(new Emitter()); readonly onWindowOpened = this._onWindowOpened.event; private readonly _onWindowReady = this._register(new Emitter()); readonly onWindowReady = this._onWindowReady.event; + private readonly _onWindowDestroyed = this._register(new Emitter()); + readonly onWindowDestroyed = this._onWindowDestroyed.event; + private readonly _onWindowsCountChanged = this._register(new Emitter()); readonly onWindowsCountChanged = this._onWindowsCountChanged.event; + private readonly windowsStateHandler = this._register(new WindowsStateHandler(this, this.stateService, this.lifecycleMainService, this.logService, this.configurationService)); + constructor( private readonly machineId: string, private readonly initialUserEnv: IProcessEnvironment, @@ -182,190 +142,20 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic @IBackupMainService private readonly backupMainService: IBackupMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService, - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IDialogMainService private readonly dialogMainService: IDialogMainService + @IDialogMainService private readonly dialogMainService: IDialogMainService, + @IFileService private readonly fileService: IFileService ) { super(); - this.windowsState = restoreWindowsState(this.stateService.getItem(WindowsMainService.windowsStateStorageKey)); - if (!Array.isArray(this.windowsState.openedWindows)) { - this.windowsState.openedWindows = []; - } - this.lifecycleMainService.when(LifecycleMainPhase.Ready).then(() => this.registerListeners()); - this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.installWindowsMutex()); - } - - private installWindowsMutex(): void { - const win32MutexName = product.win32MutexName; - if (isWindows && win32MutexName) { - try { - const WindowsMutex = (require.__$__nodeRequire('windows-mutex') as typeof import('windows-mutex')).Mutex; - const mutex = new WindowsMutex(win32MutexName); - once(this.lifecycleMainService.onWillShutdown)(() => mutex.release()); - } catch (e) { - this.logService.error(e); - } - } } private registerListeners(): void { - // When a window looses focus, save all windows state. This allows to - // prevent loss of window-state data when OS is restarted without properly - // shutting down the application (https://github.com/microsoft/vscode/issues/87171) - app.on('browser-window-blur', () => { - if (!this.shuttingDown) { - this.saveWindowsState(); - } - }); - - // Handle various lifecycle events around windows - this.lifecycleMainService.onBeforeWindowClose(window => this.onBeforeWindowClose(window)); - this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown()); - this.onWindowsCountChanged(e => { - if (e.newCount - e.oldCount > 0) { - // clear last closed window state when a new window opens. this helps on macOS where - // otherwise closing the last window, opening a new window and then quitting would - // use the state of the previously closed window when restarting. - this.lastClosedWindowState = undefined; - } - }); - // Signal a window is ready after having entered a workspace - this._register(this.workspacesMainService.onWorkspaceEntered(event => { - this._onWindowReady.fire(event.window); - })); - } - - // Note that onBeforeShutdown() and onBeforeWindowClose() are fired in different order depending on the OS: - // - macOS: since the app will not quit when closing the last window, you will always first get - // the onBeforeShutdown() event followed by N onBeforeWindowClose() events for each window - // - other: on other OS, closing the last window will quit the app so the order depends on the - // user interaction: closing the last window will first trigger onBeforeWindowClose() - // and then onBeforeShutdown(). Using the quit action however will first issue onBeforeShutdown() - // and then onBeforeWindowClose(). - // - // Here is the behavior on different OS depending on action taken (Electron 1.7.x): - // - // Legend - // - quit(N): quit application with N windows opened - // - close(1): close one window via the window close button - // - closeAll: close all windows via the taskbar command - // - onBeforeShutdown(N): number of windows reported in this event handler - // - onBeforeWindowClose(N, M): number of windows reported and quitRequested boolean in this event handler - // - // macOS - // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) - // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) - // - quit(0): onBeforeShutdown(0) - // - close(1): onBeforeWindowClose(1, false) - // - // Windows - // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) - // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) - // - close(1): onBeforeWindowClose(2, false)[not last window] - // - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window] - // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) - // - // Linux - // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) - // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) - // - close(1): onBeforeWindowClose(2, false)[not last window] - // - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window] - // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) - // - private onBeforeShutdown(): void { - this.shuttingDown = true; - - this.saveWindowsState(); - } - - private saveWindowsState(): void { - const currentWindowsState: IWindowsState = { - openedWindows: [], - lastPluginDevelopmentHostWindow: this.windowsState.lastPluginDevelopmentHostWindow, - lastActiveWindow: this.lastClosedWindowState - }; - - // 1.) Find a last active window (pick any other first window otherwise) - if (!currentWindowsState.lastActiveWindow) { - let activeWindow = this.getLastActiveWindow(); - if (!activeWindow || activeWindow.isExtensionDevelopmentHost) { - activeWindow = WindowsMainService.WINDOWS.find(window => !window.isExtensionDevelopmentHost); - } - - if (activeWindow) { - currentWindowsState.lastActiveWindow = this.toWindowState(activeWindow); - } - } - - // 2.) Find extension host window - const extensionHostWindow = WindowsMainService.WINDOWS.find(window => window.isExtensionDevelopmentHost && !window.isExtensionTestHost); - if (extensionHostWindow) { - currentWindowsState.lastPluginDevelopmentHostWindow = this.toWindowState(extensionHostWindow); - } - - // 3.) All windows (except extension host) for N >= 2 to support `restoreWindows: all` or for auto update - // - // Careful here: asking a window for its window state after it has been closed returns bogus values (width: 0, height: 0) - // so if we ever want to persist the UI state of the last closed window (window count === 1), it has - // to come from the stored lastClosedWindowState on Win/Linux at least - if (this.getWindowCount() > 1) { - currentWindowsState.openedWindows = WindowsMainService.WINDOWS.filter(window => !window.isExtensionDevelopmentHost).map(window => this.toWindowState(window)); - } - - // Persist - const state = getWindowsStateStoreData(currentWindowsState); - this.stateService.setItem(WindowsMainService.windowsStateStorageKey, state); - - if (this.shuttingDown) { - this.logService.trace('onBeforeShutdown', state); - } - } - - // See note on #onBeforeShutdown() for details how these events are flowing - private onBeforeWindowClose(win: ICodeWindow): void { - if (this.lifecycleMainService.quitRequested) { - return; // during quit, many windows close in parallel so let it be handled in the before-quit handler - } - - // On Window close, update our stored UI state of this window - const state: IWindowState = this.toWindowState(win); - if (win.isExtensionDevelopmentHost && !win.isExtensionTestHost) { - this.windowsState.lastPluginDevelopmentHostWindow = state; // do not let test run window state overwrite our extension development state - } - - // Any non extension host window with same workspace or folder - else if (!win.isExtensionDevelopmentHost && (!!win.openedWorkspace || !!win.openedFolderUri)) { - this.windowsState.openedWindows.forEach(o => { - const sameWorkspace = win.openedWorkspace && o.workspace && o.workspace.id === win.openedWorkspace.id; - const sameFolder = win.openedFolderUri && o.folderUri && extUriBiasedIgnorePathCase.isEqual(o.folderUri, win.openedFolderUri); - - if (sameWorkspace || sameFolder) { - o.uiState = state.uiState; - } - }); - } - - // On Windows and Linux closing the last window will trigger quit. Since we are storing all UI state - // before quitting, we need to remember the UI state of this window to be able to persist it. - // On macOS we keep the last closed window state ready in case the user wants to quit right after or - // wants to open another window, in which case we use this state over the persisted one. - if (this.getWindowCount() === 1) { - this.lastClosedWindowState = state; - } - } - - private toWindowState(win: ICodeWindow): IWindowState { - return { - workspace: win.openedWorkspace, - folderUri: win.openedFolderUri, - backupPath: win.backupPath, - remoteAuthority: win.remoteAuthority, - uiState: win.serializeWindowState() - }; + this._register(this.workspacesManagementMainService.onWorkspaceEntered(event => this._onWindowReady.fire(event.window))); } openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[] { @@ -375,18 +165,22 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic cli = { ...cli, remote }; } + const forceEmpty = true; const forceReuseWindow = options?.forceReuseWindow; const forceNewWindow = !forceReuseWindow; - return this.open({ ...openConfig, cli, forceEmpty: true, forceNewWindow, forceReuseWindow }); + return this.open({ ...openConfig, cli, forceEmpty, forceNewWindow, forceReuseWindow }); } open(openConfig: IOpenConfiguration): ICodeWindow[] { this.logService.trace('windowsManager#open'); - openConfig = this.validateOpenConfig(openConfig); - const foldersToAdd: IFolderPathToOpen[] = []; - const foldersToOpen: IFolderPathToOpen[] = []; + if (openConfig.addMode && (openConfig.initialStartup || !this.getLastActiveWindow())) { + openConfig.addMode = false; // Make sure addMode is only enabled if we have an active window + } + + const foldersToAdd: ISingleFolderWorkspacePathToOpen[] = []; + const foldersToOpen: ISingleFolderWorkspacePathToOpen[] = []; const workspacesToOpen: IWorkspacePathToOpen[] = []; const workspacesToRestore: IWorkspacePathToOpen[] = []; const emptyToRestore: IEmptyWindowBackupInfo[] = []; @@ -397,7 +191,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const pathsToOpen = this.getPathsToOpen(openConfig); this.logService.trace('windowsManager#open pathsToOpen', pathsToOpen); for (const path of pathsToOpen) { - if (isFolderPathToOpen(path)) { + if (isSingleFolderWorkspacePathToOpen(path)) { if (openConfig.addMode) { // When run with --add, take the folders that are to be opened as // folders that should be added to the currently active window. @@ -431,13 +225,11 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic filesToOpen.filesToWait = { paths: [...filesToOpen.filesToDiff, ...filesToOpen.filesToOpenOrCreate], waitMarkerFileUri: openConfig.waitMarkerFileURI }; } - // // These are windows to restore because of hot-exit or from previous session (only performed once on startup!) - // if (openConfig.initialStartup) { // Untitled workspaces are always restored - workspacesToRestore.push(...this.workspacesMainService.getUntitledWorkspacesSync()); + workspacesToRestore.push(...this.workspacesManagementMainService.getUntitledWorkspacesSync()); workspacesToOpen.push(...workspacesToRestore); // Empty windows with backups are always restored @@ -461,13 +253,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Otherwise, find a good window based on open params else { - const focusLastActive = this.windowsState.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length); + const focusLastActive = this.windowsStateHandler.state.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length); let focusLastOpened = true; let focusLastWindow = true; // 2.) focus last active window if we are not instructed to open any paths if (focusLastActive) { - const lastActiveWindow = usedWindows.filter(window => this.windowsState.lastActiveWindow && window.backupPath === this.windowsState.lastActiveWindow.backupPath); + const lastActiveWindow = usedWindows.filter(window => this.windowsStateHandler.state.lastActiveWindow && window.backupPath === this.windowsStateHandler.state.lastActiveWindow.backupPath); if (lastActiveWindow.length) { lastActiveWindow[0].focus(); focusLastOpened = false; @@ -504,11 +296,11 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const isDiff = filesToOpen && filesToOpen.filesToDiff.length > 0; if (!usedWindows.some(window => window.isExtensionDevelopmentHost) && !isDiff && !openConfig.noRecentEntry) { const recents: IRecent[] = []; - for (let pathToOpen of pathsToOpen) { - if (pathToOpen.workspace) { + for (const pathToOpen of pathsToOpen) { + if (isWorkspacePathToOpen(pathToOpen)) { recents.push({ label: pathToOpen.label, workspace: pathToOpen.workspace }); - } else if (pathToOpen.folderUri) { - recents.push({ label: pathToOpen.label, folderUri: pathToOpen.folderUri }); + } else if (isSingleFolderWorkspacePathToOpen(pathToOpen)) { + recents.push({ label: pathToOpen.label, folderUri: pathToOpen.workspace.uri }); } else if (pathToOpen.fileUri) { recents.push({ label: pathToOpen.label, fileUri: pathToOpen.fileUri }); } @@ -522,33 +314,34 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // process can continue. We do this by deleting the waitMarkerFilePath. const waitMarkerFileURI = openConfig.waitMarkerFileURI; if (openConfig.context === OpenContext.CLI && waitMarkerFileURI && usedWindows.length === 1 && usedWindows[0]) { - usedWindows[0].whenClosedOrLoaded.then(() => unlink(waitMarkerFileURI.fsPath, () => undefined)); + usedWindows[0].whenClosedOrLoaded.then(() => this.fileService.del(waitMarkerFileURI), () => undefined); } return usedWindows; } - private validateOpenConfig(config: IOpenConfiguration): IOpenConfiguration { - - // Make sure addMode is only enabled if we have an active window - if (config.addMode && (config.initialStartup || !this.getLastActiveWindow())) { - config.addMode = false; - } - - return config; - } - private doOpen( openConfig: IOpenConfiguration, workspacesToOpen: IWorkspacePathToOpen[], - foldersToOpen: IFolderPathToOpen[], + foldersToOpen: ISingleFolderWorkspacePathToOpen[], emptyToRestore: IEmptyWindowBackupInfo[], emptyToOpen: number, filesToOpen: IFilesToOpen | undefined, - foldersToAdd: IFolderPathToOpen[] + foldersToAdd: ISingleFolderWorkspacePathToOpen[] ): { windows: ICodeWindow[], filesOpenedInWindow: ICodeWindow | undefined } { + + // Keep track of used windows and remember + // if files have been opened in one of them const usedWindows: ICodeWindow[] = []; let filesOpenedInWindow: ICodeWindow | undefined = undefined; + function addUsedWindow(window: ICodeWindow, openedFiles?: boolean): void { + usedWindows.push(window); + + if (openedFiles) { + filesOpenedInWindow = window; + filesToOpen = undefined; // reset `filesToOpen` since files have been opened + } + } // Settings can decide if files/folders open in new window or not let { openFolderInNewWindow, openFilesInNewWindow } = this.shouldOpenNewWindow(openConfig); @@ -558,57 +351,59 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const authority = foldersToAdd[0].remoteAuthority; const lastActiveWindow = this.getLastActiveWindowForAuthority(authority); if (lastActiveWindow) { - usedWindows.push(this.doAddFoldersToExistingWindow(lastActiveWindow, foldersToAdd.map(f => f.folderUri))); + addUsedWindow(this.doAddFoldersToExistingWindow(lastActiveWindow, foldersToAdd.map(folderToAdd => folderToAdd.workspace.uri))); } } - // Handle files to open/diff or to create when we dont open a folder and we do not restore any folder/untitled from hot-exit - const potentialWindowsCount = foldersToOpen.length + workspacesToOpen.length + emptyToRestore.length; - if (potentialWindowsCount === 0 && filesToOpen) { + // Handle files to open/diff or to create when we dont open a folder and we do not restore any + // folder/untitled from hot-exit by trying to open them in the window that fits best + const potentialNewWindowsCount = foldersToOpen.length + workspacesToOpen.length + emptyToRestore.length; + if (filesToOpen && potentialNewWindowsCount === 0) { // Find suitable window or folder path to open files in const fileToCheck = filesToOpen.filesToOpenOrCreate[0] || filesToOpen.filesToDiff[0]; // only look at the windows with correct authority - const windows = WindowsMainService.WINDOWS.filter(window => filesToOpen && window.remoteAuthority === filesToOpen.remoteAuthority); + const windows = this.getWindows().filter(window => filesToOpen && window.remoteAuthority === filesToOpen.remoteAuthority); - const bestWindowOrFolder = findBestWindowOrFolderForFile({ - windows, - newWindow: openFilesInNewWindow, - context: openConfig.context, - fileUri: fileToCheck?.fileUri, - localWorkspaceResolver: workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesMainService.resolveLocalWorkspaceSync(workspace.configPath) : null - }); + // figure out a good window to open the files in if any + // with a fallback to the last active window. + // + // in case `openFilesInNewWindow` is enforced, we skip + // this step. + let windowToUseForFiles: ICodeWindow | undefined = undefined; + if (fileToCheck?.fileUri && !openFilesInNewWindow) { + if (openConfig.context === OpenContext.DESKTOP || openConfig.context === OpenContext.CLI || openConfig.context === OpenContext.DOCK) { + windowToUseForFiles = findWindowOnFile(windows, fileToCheck.fileUri, workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesManagementMainService.resolveLocalWorkspaceSync(workspace.configPath) : null); + } + + if (!windowToUseForFiles) { + windowToUseForFiles = this.doGetLastActiveWindow(windows); + } + } // We found a window to open the files in - if (bestWindowOrFolder instanceof CodeWindow) { + if (windowToUseForFiles) { // Window is workspace - if (bestWindowOrFolder.openedWorkspace) { - workspacesToOpen.push({ workspace: bestWindowOrFolder.openedWorkspace, remoteAuthority: bestWindowOrFolder.remoteAuthority }); + if (isWorkspaceIdentifier(windowToUseForFiles.openedWorkspace)) { + workspacesToOpen.push({ workspace: windowToUseForFiles.openedWorkspace, remoteAuthority: windowToUseForFiles.remoteAuthority }); } // Window is single folder - else if (bestWindowOrFolder.openedFolderUri) { - foldersToOpen.push({ folderUri: bestWindowOrFolder.openedFolderUri, remoteAuthority: bestWindowOrFolder.remoteAuthority }); + else if (isSingleFolderWorkspaceIdentifier(windowToUseForFiles.openedWorkspace)) { + foldersToOpen.push({ workspace: windowToUseForFiles.openedWorkspace, remoteAuthority: windowToUseForFiles.remoteAuthority }); } // Window is empty else { - - // Do open files - const window = this.doOpenFilesInExistingWindow(openConfig, bestWindowOrFolder, filesToOpen); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - filesToOpen = undefined; - filesOpenedInWindow = window; + addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowToUseForFiles, filesToOpen), true); } } // Finally, if no window or folder is found, just open the files in an empty window else { - const window = this.openInBrowserWindow({ + addUsedWindow(this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, @@ -616,12 +411,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic forceNewWindow: true, remoteAuthority: filesToOpen.remoteAuthority, forceNewTabbedWindow: openConfig.forceNewTabbedWindow - }); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - filesToOpen = undefined; - filesOpenedInWindow = window; + }), true); } } @@ -630,27 +420,20 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (allWorkspacesToOpen.length > 0) { // Check for existing instances - const windowsOnWorkspace = coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspace(WindowsMainService.WINDOWS, workspaceToOpen.workspace))); + const windowsOnWorkspace = coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspaceOrFolder(this.getWindows(), workspaceToOpen.workspace.configPath))); if (windowsOnWorkspace.length > 0) { const windowOnWorkspace = windowsOnWorkspace[0]; const filesToOpenInWindow = (filesToOpen?.remoteAuthority === windowOnWorkspace.remoteAuthority) ? filesToOpen : undefined; // Do open files - const window = this.doOpenFilesInExistingWindow(openConfig, windowOnWorkspace, filesToOpenInWindow); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - if (filesToOpenInWindow) { - filesToOpen = undefined; - filesOpenedInWindow = window; - } + addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowOnWorkspace, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then } // Open remaining ones allWorkspacesToOpen.forEach(workspaceToOpen => { - if (windowsOnWorkspace.some(win => win.openedWorkspace && win.openedWorkspace.id === workspaceToOpen.workspace.id)) { + if (windowsOnWorkspace.some(window => window.openedWorkspace && window.openedWorkspace.id === workspaceToOpen.workspace.id)) { return; // ignore folders that are already open } @@ -658,46 +441,31 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; // Do open folder - const window = this.doOpenFolderOrWorkspace(openConfig, workspaceToOpen, openFolderInNewWindow, filesToOpenInWindow); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - if (filesToOpenInWindow) { - filesToOpen = undefined; - filesOpenedInWindow = window; - } + addUsedWindow(this.doOpenFolderOrWorkspace(openConfig, workspaceToOpen, openFolderInNewWindow, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then }); } // Handle folders to open (instructed and to restore) - const allFoldersToOpen = distinct(foldersToOpen, folder => extUriBiasedIgnorePathCase.getComparisonKey(folder.folderUri)); // prevent duplicates + const allFoldersToOpen = distinct(foldersToOpen, folder => extUriBiasedIgnorePathCase.getComparisonKey(folder.workspace.uri)); // prevent duplicates if (allFoldersToOpen.length > 0) { // Check for existing instances - const windowsOnFolderPath = coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspace(WindowsMainService.WINDOWS, folderToOpen.folderUri))); + const windowsOnFolderPath = coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspaceOrFolder(this.getWindows(), folderToOpen.workspace.uri))); if (windowsOnFolderPath.length > 0) { const windowOnFolderPath = windowsOnFolderPath[0]; const filesToOpenInWindow = filesToOpen?.remoteAuthority === windowOnFolderPath.remoteAuthority ? filesToOpen : undefined; // Do open files - const window = this.doOpenFilesInExistingWindow(openConfig, windowOnFolderPath, filesToOpenInWindow); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - if (filesToOpenInWindow) { - filesToOpen = undefined; - filesOpenedInWindow = window; - } + addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowOnFolderPath, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then } // Open remaining ones allFoldersToOpen.forEach(folderToOpen => { - - if (windowsOnFolderPath.some(win => extUriBiasedIgnorePathCase.isEqual(win.openedFolderUri, folderToOpen.folderUri))) { + if (windowsOnFolderPath.some(window => isSingleFolderWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.uri, folderToOpen.workspace.uri))) { return; // ignore folders that are already open } @@ -705,14 +473,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; // Do open folder - const window = this.doOpenFolderOrWorkspace(openConfig, folderToOpen, openFolderInNewWindow, filesToOpenInWindow); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - if (filesToOpenInWindow) { - filesToOpen = undefined; - filesOpenedInWindow = window; - } + addUsedWindow(this.doOpenFolderOrWorkspace(openConfig, folderToOpen, openFolderInNewWindow, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then }); @@ -725,7 +486,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const remoteAuthority = emptyWindowBackupInfo.remoteAuthority; const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; - const window = this.openInBrowserWindow({ + addUsedWindow(this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, @@ -734,14 +495,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic forceNewWindow: true, forceNewTabbedWindow: openConfig.forceNewTabbedWindow, emptyWindowBackupInfo - }); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - if (filesToOpenInWindow) { - filesToOpen = undefined; - filesOpenedInWindow = window; - } + }), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then }); @@ -756,14 +510,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const remoteAuthority = filesToOpen ? filesToOpen.remoteAuthority : (openConfig.cli && openConfig.cli.remote || undefined); for (let i = 0; i < emptyToOpen; i++) { - const window = this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, filesToOpen); - usedWindows.push(window); - - // Reset `filesToOpen` because we handled them and also remember window we used - if (filesToOpen) { - filesToOpen = undefined; - filesOpenedInWindow = window; - } + addUsedWindow(this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, filesToOpen), !!filesToOpen); // any other window to open must open in new window then openFolderInNewWindow = true; @@ -778,23 +525,20 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic window.focus(); // make sure window has focus - const params: { filesToOpenOrCreate?: IPath[], filesToDiff?: IPath[], filesToWait?: IPathsToWaitFor, termProgram?: string } = {}; - if (filesToOpen) { - params.filesToOpenOrCreate = filesToOpen.filesToOpenOrCreate; - params.filesToDiff = filesToOpen.filesToDiff; - params.filesToWait = filesToOpen.filesToWait; - } - - if (configuration.userEnv) { - params.termProgram = configuration.userEnv['TERM_PROGRAM']; - } - + const params: INativeOpenFileRequest = { + filesToOpenOrCreate: filesToOpen?.filesToOpenOrCreate, + filesToDiff: filesToOpen?.filesToDiff, + filesToWait: filesToOpen?.filesToWait, + termProgram: configuration?.userEnv?.['TERM_PROGRAM'] + }; window.sendWhenReady('vscode:openFiles', CancellationToken.None, params); return window; } private doAddFoldersToExistingWindow(window: ICodeWindow, foldersToAdd: URI[]): ICodeWindow { + this.logService.trace('windowsManager#doAddFoldersToExistingWindow'); + window.focus(); // make sure window has focus const request: IAddFoldersRequest = { foldersToAdd }; @@ -820,50 +564,57 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }); } - private doOpenFolderOrWorkspace(openConfig: IOpenConfiguration, folderOrWorkspace: IPathToOpen, forceNewWindow: boolean, filesToOpen: IFilesToOpen | undefined, windowToUse?: ICodeWindow): ICodeWindow { + private doOpenFolderOrWorkspace(openConfig: IOpenConfiguration, folderOrWorkspace: IWorkspacePathToOpen | ISingleFolderWorkspacePathToOpen, forceNewWindow: boolean, filesToOpen: IFilesToOpen | undefined, windowToUse?: ICodeWindow): ICodeWindow { if (!forceNewWindow && !windowToUse && typeof openConfig.contextWindowId === 'number') { windowToUse = this.getWindowById(openConfig.contextWindowId); // fix for https://github.com/microsoft/vscode/issues/49587 } return this.openInBrowserWindow({ + workspace: folderOrWorkspace.workspace, userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, - workspace: folderOrWorkspace.workspace, - folderUri: folderOrWorkspace.folderUri, - filesToOpen, remoteAuthority: folderOrWorkspace.remoteAuthority, forceNewWindow, forceNewTabbedWindow: openConfig.forceNewTabbedWindow, + filesToOpen, windowToUse }); } private getPathsToOpen(openConfig: IOpenConfiguration): IPathToOpen[] { - let windowsToOpen: IPathToOpen[]; + let pathsToOpen: IPathToOpen[]; let isCommandLineOrAPICall = false; let restoredWindows = false; // Extract paths: from API if (openConfig.urisToOpen && openConfig.urisToOpen.length > 0) { - windowsToOpen = this.doExtractPathsFromAPI(openConfig); + pathsToOpen = this.doExtractPathsFromAPI(openConfig); isCommandLineOrAPICall = true; } // Check for force empty else if (openConfig.forceEmpty) { - windowsToOpen = [Object.create(null)]; + pathsToOpen = [Object.create(null)]; } // Extract paths: from CLI else if (openConfig.cli._.length || openConfig.cli['folder-uri'] || openConfig.cli['file-uri']) { - windowsToOpen = this.doExtractPathsFromCLI(openConfig.cli); + pathsToOpen = this.doExtractPathsFromCLI(openConfig.cli); + if (pathsToOpen.length === 0) { + pathsToOpen.push(Object.create(null)); // add an empty window if we did not have windows to open from command line + } + isCommandLineOrAPICall = true; } - // Extract windows: from previous session + // Extract paths: from previous session else { - windowsToOpen = this.doGetWindowsFromLastSession(); + pathsToOpen = this.doGetPathsFromLastSession(); + if (pathsToOpen.length === 0) { + pathsToOpen.push(Object.create(null)); // add an empty window if we did not have windows to restore + } + restoredWindows = true; } @@ -872,15 +623,15 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // If we are in `addMode`, we should not do this because in that case all // folders should be added to the existing window. if (!openConfig.addMode && isCommandLineOrAPICall) { - const foldersToOpen = windowsToOpen.filter(path => !!path.folderUri); + const foldersToOpen = pathsToOpen.filter(path => isSingleFolderWorkspacePathToOpen(path)) as ISingleFolderWorkspacePathToOpen[]; if (foldersToOpen.length > 1) { const remoteAuthority = foldersToOpen[0].remoteAuthority; - if (foldersToOpen.every(f => f.remoteAuthority === remoteAuthority)) { // only if all folder have the same authority - const workspace = this.workspacesMainService.createUntitledWorkspaceSync(foldersToOpen.map(folder => ({ uri: folder.folderUri! }))); + if (foldersToOpen.every(folderToOpen => folderToOpen.remoteAuthority === remoteAuthority)) { // only if all folder have the same authority + const workspace = this.workspacesManagementMainService.createUntitledWorkspaceSync(foldersToOpen.map(folder => ({ uri: folder.workspace.uri }))); // Add workspace and remove folders thereby - windowsToOpen.push({ workspace, remoteAuthority }); - windowsToOpen = windowsToOpen.filter(path => !path.folderUri); + pathsToOpen.push({ workspace, remoteAuthority }); + pathsToOpen = pathsToOpen.filter(path => !isSingleFolderWorkspacePathToOpen(path)); } } } @@ -891,63 +642,57 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Use `unshift` to ensure any new window to open comes last // for proper focus treatment. if (openConfig.initialStartup && !restoredWindows && this.configurationService.getValue('window')?.restoreWindows === 'preserve') { - windowsToOpen.unshift(...this.doGetWindowsFromLastSession().filter(window => window.workspace || window.folderUri || window.backupPath)); + pathsToOpen.unshift(...this.doGetPathsFromLastSession().filter(path => isWorkspacePathToOpen(path) || isSingleFolderWorkspacePathToOpen(path) || path.backupPath)); } - return windowsToOpen; + return pathsToOpen; } private doExtractPathsFromAPI(openConfig: IOpenConfiguration): IPathToOpen[] { const pathsToOpen: IPathToOpen[] = []; - const parseOptions: IPathParseOptions = { gotoLineMode: openConfig.gotoLineMode }; - for (const pathToOpen of openConfig.urisToOpen || []) { - if (!pathToOpen) { - continue; - } + const pathResolveOptions: IPathResolveOptions = { gotoLineMode: openConfig.gotoLineMode }; + for (const pathToOpen of coalesce(openConfig.urisToOpen || [])) { + const path = this.resolveOpenable(pathToOpen, pathResolveOptions); - const path = this.parseUri(pathToOpen, parseOptions); + // Path exists if (path) { path.label = pathToOpen.label; pathsToOpen.push(path); - } else { - const uri = this.resourceFromURIToOpen(pathToOpen); + } - // Warn about the invalid URI or path - let message, detail; - if (uri.scheme === Schemas.file) { - message = localize('pathNotExistTitle', "Path does not exist"); - detail = localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", getPathLabel(uri.fsPath, this.environmentService)); - } else { - message = localize('uriInvalidTitle', "URI can not be opened"); - detail = localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString()); - } + // Path does not exist: show a warning box + else { + const uri = this.resourceFromOpenable(pathToOpen); const options: MessageBoxOptions = { title: product.nameLong, type: 'info', buttons: [localize('ok', "OK")], - message, - detail, + message: uri.scheme === Schemas.file ? localize('pathNotExistTitle', "Path does not exist") : localize('uriInvalidTitle', "URI can not be opened"), + detail: uri.scheme === Schemas.file ? + localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", getPathLabel(uri.fsPath, this.environmentService)) : + localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString()), noLink: true }; this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); } } + return pathsToOpen; } private doExtractPathsFromCLI(cli: NativeParsedArgs): IPath[] { const pathsToOpen: IPathToOpen[] = []; - const parseOptions: IPathParseOptions = { ignoreFileNotFound: true, gotoLineMode: cli.goto, remoteAuthority: cli.remote || undefined }; + const pathResolveOptions: IPathResolveOptions = { ignoreFileNotFound: true, gotoLineMode: cli.goto, remoteAuthority: cli.remote || undefined }; // folder uris const folderUris = cli['folder-uri']; if (folderUris) { - for (let f of folderUris) { - const folderUri = this.argToUri(f); + for (const rawFolderUri of folderUris) { + const folderUri = this.cliArgToUri(rawFolderUri); if (folderUri) { - const path = this.parseUri({ folderUri }, parseOptions); + const path = this.resolveOpenable({ folderUri }, pathResolveOptions); if (path) { pathsToOpen.push(path); } @@ -958,10 +703,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // file uris const fileUris = cli['file-uri']; if (fileUris) { - for (let f of fileUris) { - const fileUri = this.argToUri(f); + for (const rawFileUri of fileUris) { + const fileUri = this.cliArgToUri(rawFileUri); if (fileUri) { - const path = this.parseUri(hasWorkspaceFileExtension(f) ? { workspaceUri: fileUri } : { fileUri }, parseOptions); + const path = this.resolveOpenable(hasWorkspaceFileExtension(rawFileUri) ? { workspaceUri: fileUri } : { fileUri }, pathResolveOptions); if (path) { pathsToOpen.push(path); } @@ -970,30 +715,42 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // folder or file paths - const cliArgs = cli._; - for (let cliArg of cliArgs) { - const path = this.parsePath(cliArg, parseOptions); + const cliPaths = cli._; + for (const cliPath of cliPaths) { + const path = this.doResolveFileOpenable(cliPath, pathResolveOptions); if (path) { pathsToOpen.push(path); } } - if (pathsToOpen.length) { - return pathsToOpen; - } - - // No path provided, return empty to open empty - return [Object.create(null)]; + return pathsToOpen; } - private doGetWindowsFromLastSession(): IPathToOpen[] { + private cliArgToUri(arg: string): URI | undefined { + try { + const uri = URI.parse(arg); + if (!uri.scheme) { + this.logService.error(`Invalid URI input string, scheme missing: ${arg}`); + + return undefined; + } + + return uri; + } catch (e) { + this.logService.error(`Invalid URI input string: ${arg}, ${e.message}`); + } + + return undefined; + } + + private doGetPathsFromLastSession(): IPathToOpen[] { const restoreWindowsSetting = this.getRestoreWindowsSetting(); switch (restoreWindowsSetting) { - // none: we always open an empty window + // none: no window to restore case 'none': - return [Object.create(null)]; + return []; // one: restore last opened workspace/folder or empty window // all: restore all windows @@ -1004,48 +761,41 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic case 'folders': // Collect previously opened windows - const openedWindows: IWindowState[] = []; + const lastSessionWindows: IWindowState[] = []; if (restoreWindowsSetting !== 'one') { - openedWindows.push(...this.windowsState.openedWindows); + lastSessionWindows.push(...this.windowsStateHandler.state.openedWindows); } - if (this.windowsState.lastActiveWindow) { - openedWindows.push(this.windowsState.lastActiveWindow); + if (this.windowsStateHandler.state.lastActiveWindow) { + lastSessionWindows.push(this.windowsStateHandler.state.lastActiveWindow); } - const windowsToOpen: IPathToOpen[] = []; - for (const openedWindow of openedWindows) { + const pathsToOpen: IPathToOpen[] = []; + for (const lastSessionWindow of lastSessionWindows) { // Workspaces - if (openedWindow.workspace) { - const pathToOpen = this.parseUri({ workspaceUri: openedWindow.workspace.configPath }, { remoteAuthority: openedWindow.remoteAuthority }); - if (pathToOpen?.workspace) { - windowsToOpen.push(pathToOpen); + if (lastSessionWindow.workspace) { + const pathToOpen = this.resolveOpenable({ workspaceUri: lastSessionWindow.workspace.configPath }, { remoteAuthority: lastSessionWindow.remoteAuthority }); + if (isWorkspacePathToOpen(pathToOpen)) { + pathsToOpen.push(pathToOpen); } } // Folders - else if (openedWindow.folderUri) { - const pathToOpen = this.parseUri({ folderUri: openedWindow.folderUri }, { remoteAuthority: openedWindow.remoteAuthority }); - if (pathToOpen?.folderUri) { - windowsToOpen.push(pathToOpen); + else if (lastSessionWindow.folderUri) { + const pathToOpen = this.resolveOpenable({ folderUri: lastSessionWindow.folderUri }, { remoteAuthority: lastSessionWindow.remoteAuthority }); + if (isSingleFolderWorkspacePathToOpen(pathToOpen)) { + pathsToOpen.push(pathToOpen); } } // Empty window, potentially editors open to be restored - else if (restoreWindowsSetting !== 'folders' && openedWindow.backupPath) { - windowsToOpen.push({ backupPath: openedWindow.backupPath, remoteAuthority: openedWindow.remoteAuthority }); + else if (restoreWindowsSetting !== 'folders' && lastSessionWindow.backupPath) { + pathsToOpen.push({ backupPath: lastSessionWindow.backupPath, remoteAuthority: lastSessionWindow.remoteAuthority }); } } - if (windowsToOpen.length > 0) { - return windowsToOpen; - } - - break; + return pathsToOpen; } - - // Always fallback to empty window - return [Object.create(null)]; } private getRestoreWindowsSetting(): RestoreWindowsSetting { @@ -1064,75 +814,48 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return restoreWindows; } - private argToUri(arg: string): URI | undefined { - try { - const uri = URI.parse(arg); - if (!uri.scheme) { - this.logService.error(`Invalid URI input string, scheme missing: ${arg}`); - return undefined; - } + private resolveOpenable(openable: IWindowOpenable, options: IPathResolveOptions = {}): IPathToOpen | undefined { - return uri; - } catch (e) { - this.logService.error(`Invalid URI input string: ${arg}, ${e.message}`); + // handle file:// openables with some extra validation + let uri = this.resourceFromOpenable(openable); + if (uri.scheme === Schemas.file) { + return this.doResolveFileOpenable(openable, options); } - return undefined; + // handle non file:// openables + return this.doResolveRemoteOpenable(openable, options); } - private parseUri(toOpen: IWindowOpenable, options: IPathParseOptions = {}): IPathToOpen | undefined { - if (!toOpen) { - return undefined; - } - - let uri = this.resourceFromURIToOpen(toOpen); - if (uri.scheme === Schemas.file) { - return this.parsePath(uri.fsPath, options, isFileToOpen(toOpen)); - } + private doResolveRemoteOpenable(openable: IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined { + let uri = this.resourceFromOpenable(openable); // open remote if either specified in the cli or if it's a remotehost URI const remoteAuthority = options.remoteAuthority || getRemoteAuthority(uri); // normalize URI - uri = normalizePath(uri); - - // remove trailing slash - uri = removeTrailingPathSeparator(uri); + uri = removeTrailingPathSeparator(normalizePath(uri)); // File - if (isFileToOpen(toOpen)) { + if (isFileToOpen(openable)) { if (options.gotoLineMode) { - const parsedPath = parseLineAndColumnAware(uri.path); - return { - fileUri: uri.with({ path: parsedPath.path }), - lineNumber: parsedPath.line, - columnNumber: parsedPath.column, - remoteAuthority - }; + const { path, line, column } = parseLineAndColumnAware(uri.path); + + return { fileUri: uri.with({ path }), lineNumber: line, columnNumber: column, remoteAuthority }; } - return { - fileUri: uri, - remoteAuthority - }; + return { fileUri: uri, remoteAuthority }; } // Workspace - else if (isWorkspaceToOpen(toOpen)) { - return { - workspace: getWorkspaceIdentifier(uri), - remoteAuthority - }; + else if (isWorkspaceToOpen(openable)) { + return { workspace: getWorkspaceIdentifier(uri), remoteAuthority }; } // Folder - return { - folderUri: uri, - remoteAuthority - }; + return { workspace: getSingleFolderWorkspaceIdentifier(uri), remoteAuthority }; } - private resourceFromURIToOpen(openable: IWindowOpenable): URI { + private resourceFromOpenable(openable: IWindowOpenable): URI { if (isWorkspaceToOpen(openable)) { return openable.workspaceUri; } @@ -1144,101 +867,113 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return openable.fileUri; } - private parsePath(anyPath: string, options: IPathParseOptions, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { - if (!anyPath) { - return undefined; + private doResolveFileOpenable(path: string, options: IPathResolveOptions): IPathToOpen | undefined; + private doResolveFileOpenable(openable: IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined; + private doResolveFileOpenable(pathOrOpenable: string | IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined { + let path: string; + let forceOpenWorkspaceAsFile = false; + if (typeof pathOrOpenable === 'string') { + path = pathOrOpenable; + } else { + path = this.resourceFromOpenable(pathOrOpenable).fsPath; + forceOpenWorkspaceAsFile = isFileToOpen(pathOrOpenable); } + // Extract line/col information from path let lineNumber: number | undefined; let columnNumber: number | undefined; if (options.gotoLineMode) { - const parsedPath = parseLineAndColumnAware(anyPath); + const parsedPath = parseLineAndColumnAware(path); lineNumber = parsedPath.line; columnNumber = parsedPath.column; - anyPath = parsedPath.path; + path = parsedPath.path; } - // open remote if either specified in the cli even if it is a local file. + // With remote: resolve path as remote URI const remoteAuthority = options.remoteAuthority; if (remoteAuthority) { - const first = anyPath.charCodeAt(0); - - // make absolute - if (first !== CharCode.Slash) { - if (isWindowsDriveLetter(first) && anyPath.charCodeAt(anyPath.charCodeAt(1)) === CharCode.Colon) { - anyPath = toSlashes(anyPath); - } - - anyPath = `/${anyPath}`; - } - - const uri = URI.from({ scheme: Schemas.vscodeRemote, authority: remoteAuthority, path: anyPath }); - - // guess the file type: If it ends with a slash it's a folder. If it has a file extension, it's a file or a workspace. By defaults it's a folder. - if (anyPath.charCodeAt(anyPath.length - 1) !== CharCode.Slash) { - if (hasWorkspaceFileExtension(anyPath)) { - if (forceOpenWorkspaceAsFile) { - return { fileUri: uri, remoteAuthority }; - } - } else if (posix.basename(anyPath).indexOf('.') !== -1) { // file name starts with a dot or has an file extension - return { fileUri: uri, remoteAuthority }; - } - } - - return { folderUri: uri, remoteAuthority }; + return this.doResolvePathRemote(path, remoteAuthority, forceOpenWorkspaceAsFile); } - let candidate = normalize(anyPath); + // Without remote: resolve path as local URI + return this.doResolvePathLocal(path, options, lineNumber, columnNumber, forceOpenWorkspaceAsFile); + } + + private doResolvePathRemote(path: string, remoteAuthority: string, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { + const first = path.charCodeAt(0); + + // make absolute + if (first !== CharCode.Slash) { + if (isWindowsDriveLetter(first) && path.charCodeAt(path.charCodeAt(1)) === CharCode.Colon) { + path = toSlashes(path); + } + + path = `/${path}`; + } + + const uri = URI.from({ scheme: Schemas.vscodeRemote, authority: remoteAuthority, path: path }); + + // guess the file type: + // - if it ends with a slash it's a folder + // - if it has a file extension, it's a file or a workspace + // - by defaults it's a folder + if (path.charCodeAt(path.length - 1) !== CharCode.Slash) { + + // file name ends with .code-workspace + if (hasWorkspaceFileExtension(path)) { + if (forceOpenWorkspaceAsFile) { + return { fileUri: uri, remoteAuthority }; + } + return { workspace: getWorkspaceIdentifier(uri), remoteAuthority }; + } + + // file name starts with a dot or has an file extension + else if (posix.basename(path).indexOf('.') !== -1) { + return { fileUri: uri, remoteAuthority }; + } + } + + return { workspace: getSingleFolderWorkspaceIdentifier(uri), remoteAuthority }; + } + + private doResolvePathLocal(path: string, options: IPathResolveOptions, lineNumber: number | undefined, columnNumber: number | undefined, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { + + // Ensure the path is normalized and absolute + path = sanitizeFilePath(normalize(path), process.env['VSCODE_CWD'] || process.cwd()); try { - const candidateStat = statSync(candidate); - if (candidateStat.isFile()) { + const pathStat = statSync(path); + if (pathStat.isFile()) { // Workspace (unless disabled via flag) if (!forceOpenWorkspaceAsFile) { - const workspace = this.workspacesMainService.resolveLocalWorkspaceSync(URI.file(candidate)); + const workspace = this.workspacesManagementMainService.resolveLocalWorkspaceSync(URI.file(path)); if (workspace) { - return { - workspace: { id: workspace.id, configPath: workspace.configPath }, - remoteAuthority: workspace.remoteAuthority, - exists: true - }; + return { workspace: { id: workspace.id, configPath: workspace.configPath }, remoteAuthority: workspace.remoteAuthority, exists: true }; } } // File - return { - fileUri: URI.file(candidate), - lineNumber, - columnNumber, - remoteAuthority, - exists: true - }; + return { fileUri: URI.file(path), lineNumber, columnNumber, exists: true }; } // Folder (we check for isDirectory() because e.g. paths like /dev/null // are neither file nor folder but some external tools might pass them // over to us) - else if (candidateStat.isDirectory()) { - return { - folderUri: URI.file(candidate), - remoteAuthority, - exists: true - }; + else if (pathStat.isDirectory()) { + return { workspace: getSingleFolderWorkspaceIdentifier(URI.file(path), pathStat), exists: true }; } } catch (error) { - const fileUri = URI.file(candidate); - this.workspacesHistoryMainService.removeRecentlyOpened([fileUri]); // since file does not seem to exist anymore, remove from recent + const fileUri = URI.file(path); + + // since file does not seem to exist anymore, remove from recent + this.workspacesHistoryMainService.removeRecentlyOpened([fileUri]); // assume this is a file that does not yet exist if (options?.ignoreFileNotFound) { - return { - fileUri, - remoteAuthority, - exists: false - }; + return { fileUri, exists: false }; } } @@ -1287,12 +1022,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return { openFolderInNewWindow: !!openFolderInNewWindow, openFilesInNewWindow }; } - openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): ICodeWindow[] { + openExtensionDevelopmentHostWindow(extensionDevelopmentPaths: string[], openConfig: IOpenConfiguration): ICodeWindow[] { // Reload an existing extension development host window on the same path // We currently do not allow more than one extension development window // on the same extension path. - const existingWindow = findWindowOnExtensionDevelopmentPath(WindowsMainService.WINDOWS, extensionDevelopmentPath); + const existingWindow = findWindowOnExtensionDevelopmentPath(this.getWindows(), extensionDevelopmentPaths); if (existingWindow) { this.lifecycleMainService.reload(existingWindow, openConfig.cli); existingWindow.focus(); // make sure it gets focus and is restored @@ -1306,10 +1041,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Fill in previously opened workspace unless an explicit path is provided and we are not unit testing if (!cliArgs.length && !folderUris.length && !fileUris.length && !openConfig.cli.extensionTestsPath) { - const extensionDevelopmentWindowState = this.windowsState.lastPluginDevelopmentHostWindow; + const extensionDevelopmentWindowState = this.windowsStateHandler.state.lastPluginDevelopmentHostWindow; const workspaceToOpen = extensionDevelopmentWindowState && (extensionDevelopmentWindowState.workspace || extensionDevelopmentWindowState.folderUri); if (workspaceToOpen) { - if (isSingleFolderWorkspaceIdentifier(workspaceToOpen)) { + if (URI.isUri(workspaceToOpen)) { if (workspaceToOpen.scheme === Schemas.file) { cliArgs = [workspaceToOpen.fsPath]; } else { @@ -1326,9 +1061,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } let authority = ''; - for (let p of extensionDevelopmentPath) { - if (p.match(/^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/)) { - const url = URI.parse(p); + for (const extensionDevelopmentPath of extensionDevelopmentPaths) { + if (extensionDevelopmentPath.match(/^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/)) { + const url = URI.parse(extensionDevelopmentPath); if (url.scheme === Schemas.vscodeRemote) { if (authority) { if (url.authority !== authority) { @@ -1347,7 +1082,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic cliArgs = cliArgs.filter(path => { const uri = URI.file(path); - if (!!findWindowOnWorkspaceOrFolderUri(WindowsMainService.WINDOWS, uri)) { + if (!!findWindowOnWorkspaceOrFolder(this.getWindows(), uri)) { return false; } @@ -1355,8 +1090,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }); folderUris = folderUris.filter(folderUriStr => { - const folderUri = this.argToUri(folderUriStr); - if (!!findWindowOnWorkspaceOrFolderUri(WindowsMainService.WINDOWS, folderUri)) { + const folderUri = this.cliArgToUri(folderUriStr); + if (folderUri && !!findWindowOnWorkspaceOrFolder(this.getWindows(), folderUri)) { return false; } @@ -1364,8 +1099,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }); fileUris = fileUris.filter(fileUriStr => { - const fileUri = this.argToUri(fileUriStr); - if (!!findWindowOnWorkspaceOrFolderUri(WindowsMainService.WINDOWS, fileUri)) { + const fileUri = this.cliArgToUri(fileUriStr); + if (fileUri && !!findWindowOnWorkspaceOrFolder(this.getWindows(), fileUri)) { return false; } @@ -1398,7 +1133,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow { - // Build INativeWindowConfiguration from config and options + // Build `INativeWindowConfiguration` from config and options const configuration = { ...options.cli } as INativeWindowConfiguration; configuration.appRoot = this.environmentService.appRoot; configuration.machineId = this.machineId; @@ -1408,7 +1143,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic configuration.userEnv = { ...this.initialUserEnv, ...options.userEnv }; configuration.isInitialStartup = options.initialStartup; configuration.workspace = options.workspace; - configuration.folderUri = options.folderUri; configuration.remoteAuthority = options.remoteAuthority; const filesToOpen = options.filesToOpen; @@ -1436,32 +1170,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // New window if (!window) { - const windowConfig = this.configurationService.getValue('window'); - const state = this.getNewWindowState(configuration); - - // Window state is not from a previous session: only allow fullscreen if we inherit it or user wants fullscreen - let allowFullscreen: boolean; - if (state.hasDefaultState) { - allowFullscreen = !!(windowConfig?.newWindowDimensions && ['fullscreen', 'inherit', 'offset'].indexOf(windowConfig.newWindowDimensions) >= 0); - } - - // Window state is from a previous session: only allow fullscreen when we got updated or user wants to restore - else { - allowFullscreen = !!(this.lifecycleMainService.wasRestarted || windowConfig?.restoreFullscreen); - - if (allowFullscreen && isMacintosh && WindowsMainService.WINDOWS.some(win => win.isFullScreen)) { - // macOS: Electron does not allow to restore multiple windows in - // fullscreen. As such, if we already restored a window in that - // state, we cannot allow more fullscreen windows. See - // https://github.com/microsoft/vscode/issues/41691 and - // https://github.com/electron/electron/issues/13077 - allowFullscreen = false; - } - } - - if (state.mode === WindowMode.Fullscreen && !allowFullscreen) { - state.mode = WindowMode.Normal; - } + const state = this.windowsStateHandler.getNewWindowState(configuration); // Create the window const createdWindow = window = this.instantiationService.createInstance(CodeWindow, { @@ -1485,12 +1194,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic this._onWindowOpened.fire(createdWindow); // Indicate number change via event - this._onWindowsCountChanged.fire({ oldCount: WindowsMainService.WINDOWS.length - 1, newCount: WindowsMainService.WINDOWS.length }); + this._onWindowsCountChanged.fire({ oldCount: this.getWindowCount() - 1, newCount: this.getWindowCount() }); // Window Events once(createdWindow.onReady)(() => this._onWindowReady.fire(createdWindow)); once(createdWindow.onClose)(() => this.onWindowClosed(createdWindow)); - once(createdWindow.onDestroy)(() => this.onBeforeWindowClose(createdWindow)); // try to save state before destroy because close will not fire + once(createdWindow.onDestroy)(() => this._onWindowDestroyed.fire(createdWindow)); createdWindow.win.webContents.removeAllListeners('devtools-reload-page'); // remove built in listener so we can handle this on our own createdWindow.win.webContents.on('devtools-reload-page', () => this.lifecycleMainService.reload(createdWindow)); @@ -1534,10 +1243,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Register window for backups if (!configuration.extensionDevelopmentPath) { - if (configuration.workspace) { + if (isWorkspaceIdentifier(configuration.workspace)) { configuration.backupPath = this.backupMainService.registerWorkspaceBackupSync({ workspace: configuration.workspace, remoteAuthority: configuration.remoteAuthority }); - } else if (configuration.folderUri) { - configuration.backupPath = this.backupMainService.registerFolderBackupSync(configuration.folderUri); + } else if (isSingleFolderWorkspaceIdentifier(configuration.workspace)) { + configuration.backupPath = this.backupMainService.registerFolderBackupSync(configuration.workspace.uri); } else { const backupFolder = options.emptyWindowBackupInfo && options.emptyWindowBackupInfo.backupFolder; configuration.backupPath = this.backupMainService.registerEmptyWindowBackupSync(backupFolder, configuration.remoteAuthority); @@ -1548,162 +1257,37 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic window.load(configuration); } - private getNewWindowState(configuration: INativeWindowConfiguration): INewWindowState { - const lastActive = this.getLastActiveWindow(); - - // Restore state unless we are running extension tests - if (!configuration.extensionTestsPath) { - - // extension development host Window - load from stored settings if any - if (!!configuration.extensionDevelopmentPath && this.windowsState.lastPluginDevelopmentHostWindow) { - return this.windowsState.lastPluginDevelopmentHostWindow.uiState; - } - - // Known Workspace - load from stored settings - const workspace = configuration.workspace; - if (workspace) { - const stateForWorkspace = this.windowsState.openedWindows.filter(o => o.workspace && o.workspace.id === workspace.id).map(o => o.uiState); - if (stateForWorkspace.length) { - return stateForWorkspace[0]; - } - } - - // Known Folder - load from stored settings - if (configuration.folderUri) { - const stateForFolder = this.windowsState.openedWindows.filter(o => o.folderUri && extUriBiasedIgnorePathCase.isEqual(o.folderUri, configuration.folderUri)).map(o => o.uiState); - if (stateForFolder.length) { - return stateForFolder[0]; - } - } - - // Empty windows with backups - else if (configuration.backupPath) { - const stateForEmptyWindow = this.windowsState.openedWindows.filter(o => o.backupPath === configuration.backupPath).map(o => o.uiState); - if (stateForEmptyWindow.length) { - return stateForEmptyWindow[0]; - } - } - - // First Window - const lastActiveState = this.lastClosedWindowState || this.windowsState.lastActiveWindow; - if (!lastActive && lastActiveState) { - return lastActiveState.uiState; - } - } - - // - // In any other case, we do not have any stored settings for the window state, so we come up with something smart - // - - // We want the new window to open on the same display that the last active one is in - let displayToUse: Display | undefined; - const displays = screen.getAllDisplays(); - - // Single Display - if (displays.length === 1) { - displayToUse = displays[0]; - } - - // Multi Display - else { - - // on mac there is 1 menu per window so we need to use the monitor where the cursor currently is - if (isMacintosh) { - const cursorPoint = screen.getCursorScreenPoint(); - displayToUse = screen.getDisplayNearestPoint(cursorPoint); - } - - // if we have a last active window, use that display for the new window - if (!displayToUse && lastActive) { - displayToUse = screen.getDisplayMatching(lastActive.getBounds()); - } - - // fallback to primary display or first display - if (!displayToUse) { - displayToUse = screen.getPrimaryDisplay() || displays[0]; - } - } - - // Compute x/y based on display bounds - // Note: important to use Math.round() because Electron does not seem to be too happy about - // display coordinates that are not absolute numbers. - let state = defaultWindowState(); - state.x = Math.round(displayToUse.bounds.x + (displayToUse.bounds.width / 2) - (state.width! / 2)); - state.y = Math.round(displayToUse.bounds.y + (displayToUse.bounds.height / 2) - (state.height! / 2)); - - // Check for newWindowDimensions setting and adjust accordingly - const windowConfig = this.configurationService.getValue('window'); - let ensureNoOverlap = true; - if (windowConfig?.newWindowDimensions) { - if (windowConfig.newWindowDimensions === 'maximized') { - state.mode = WindowMode.Maximized; - ensureNoOverlap = false; - } else if (windowConfig.newWindowDimensions === 'fullscreen') { - state.mode = WindowMode.Fullscreen; - ensureNoOverlap = false; - } else if ((windowConfig.newWindowDimensions === 'inherit' || windowConfig.newWindowDimensions === 'offset') && lastActive) { - const lastActiveState = lastActive.serializeWindowState(); - if (lastActiveState.mode === WindowMode.Fullscreen) { - state.mode = WindowMode.Fullscreen; // only take mode (fixes https://github.com/microsoft/vscode/issues/19331) - } else { - state = lastActiveState; - } - - ensureNoOverlap = state.mode !== WindowMode.Fullscreen && windowConfig.newWindowDimensions === 'offset'; - } - } - - if (ensureNoOverlap) { - state = this.ensureNoOverlap(state); - } - - (state as INewWindowState).hasDefaultState = true; // flag as default state - - return state; - } - - private ensureNoOverlap(state: ISingleWindowState): ISingleWindowState { - if (WindowsMainService.WINDOWS.length === 0) { - return state; - } - - state.x = typeof state.x === 'number' ? state.x : 0; - state.y = typeof state.y === 'number' ? state.y : 0; - - const existingWindowBounds = WindowsMainService.WINDOWS.map(win => win.getBounds()); - while (existingWindowBounds.some(b => b.x === state.x || b.y === state.y)) { - state.x += 30; - state.y += 30; - } - - return state; - } - - private onWindowClosed(win: ICodeWindow): void { + private onWindowClosed(window: ICodeWindow): void { // Remove from our list so that Electron can clean it up - const index = WindowsMainService.WINDOWS.indexOf(win); + const index = WindowsMainService.WINDOWS.indexOf(window); WindowsMainService.WINDOWS.splice(index, 1); // Emit - this._onWindowsCountChanged.fire({ oldCount: WindowsMainService.WINDOWS.length + 1, newCount: WindowsMainService.WINDOWS.length }); + this._onWindowsCountChanged.fire({ oldCount: this.getWindowCount() + 1, newCount: this.getWindowCount() }); } getFocusedWindow(): ICodeWindow | undefined { - const win = BrowserWindow.getFocusedWindow(); - if (win) { - return this.getWindowById(win.id); + const window = BrowserWindow.getFocusedWindow(); + if (window) { + return this.getWindowById(window.id); } return undefined; } getLastActiveWindow(): ICodeWindow | undefined { - return getLastActiveWindow(WindowsMainService.WINDOWS); + return this.doGetLastActiveWindow(this.getWindows()); } private getLastActiveWindowForAuthority(remoteAuthority: string | undefined): ICodeWindow | undefined { - return getLastActiveWindow(WindowsMainService.WINDOWS.filter(window => window.remoteAuthority === remoteAuthority)); + return this.doGetLastActiveWindow(this.getWindows().filter(window => window.remoteAuthority === remoteAuthority)); + } + + private doGetLastActiveWindow(windows: ICodeWindow[]): ICodeWindow | undefined { + const lastFocusedDate = Math.max.apply(Math, windows.map(window => window.lastFocusTime)); + + return windows.find(window => window.lastFocusTime === lastFocusedDate); } sendToFocused(channel: string, ...args: any[]): void { @@ -1715,7 +1299,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void { - for (const window of WindowsMainService.WINDOWS) { + for (const window of this.getWindows()) { if (windowIdsToIgnore && windowIdsToIgnore.indexOf(window.id) >= 0) { continue; // do not send if we are instructed to ignore it } @@ -1724,10 +1308,18 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } } - getWindowById(windowId: number): ICodeWindow | undefined { - const res = WindowsMainService.WINDOWS.filter(window => window.id === windowId); + getWindows(): ICodeWindow[] { + return WindowsMainService.WINDOWS; + } - return firstOrDefault(res); + getWindowCount(): number { + return WindowsMainService.WINDOWS.length; + } + + getWindowById(windowId: number): ICodeWindow | undefined { + const windows = this.getWindows().filter(window => window.id === windowId); + + return firstOrDefault(windows); } getWindowByWebContents(webContents: WebContents): ICodeWindow | undefined { @@ -1738,12 +1330,4 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return this.getWindowById(browserWindow.id); } - - getWindows(): ICodeWindow[] { - return WindowsMainService.WINDOWS; - } - - getWindowCount(): number { - return WindowsMainService.WINDOWS.length; - } } diff --git a/src/vs/platform/windows/electron-main/windowsStateHandler.ts b/src/vs/platform/windows/electron-main/windowsStateHandler.ts new file mode 100644 index 000000000..9b9558fa9 --- /dev/null +++ b/src/vs/platform/windows/electron-main/windowsStateHandler.ts @@ -0,0 +1,450 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { app, Display, screen } from 'electron'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { isMacintosh } from 'vs/base/common/platform'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { defaultWindowState } from 'vs/code/electron-main/window'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IStateService } from 'vs/platform/state/node/state'; +import { INativeWindowConfiguration, IWindowSettings } from 'vs/platform/windows/common/windows'; +import { ICodeWindow, IWindowsMainService, IWindowState as IWindowUIState, WindowMode } from 'vs/platform/windows/electron-main/windows'; +import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; + +export interface IWindowState { + workspace?: IWorkspaceIdentifier; + folderUri?: URI; + backupPath?: string; + remoteAuthority?: string; + uiState: IWindowUIState; +} + +export interface IWindowsState { + lastActiveWindow?: IWindowState; + lastPluginDevelopmentHostWindow?: IWindowState; + openedWindows: IWindowState[]; +} + +interface INewWindowState extends IWindowUIState { + hasDefaultState?: boolean; +} + +interface ISerializedWindowsState { + readonly lastActiveWindow?: ISerializedWindowState; + readonly lastPluginDevelopmentHostWindow?: ISerializedWindowState; + readonly openedWindows: ISerializedWindowState[]; +} + +interface ISerializedWindowState { + readonly workspaceIdentifier?: { id: string; configURIPath: string }; + readonly folder?: string; + readonly backupPath?: string; + readonly remoteAuthority?: string; + readonly uiState: IWindowUIState; +} + +export class WindowsStateHandler extends Disposable { + + private static readonly windowsStateStorageKey = 'windowsState'; + + get state() { return this._state; } + private readonly _state = restoreWindowsState(this.stateService.getItem(WindowsStateHandler.windowsStateStorageKey)); + + private lastClosedState: IWindowState | undefined = undefined; + + private shuttingDown = false; + + constructor( + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + @IStateService private readonly stateService: IStateService, + @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, + @ILogService private readonly logService: ILogService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { + + // When a window looses focus, save all windows state. This allows to + // prevent loss of window-state data when OS is restarted without properly + // shutting down the application (https://github.com/microsoft/vscode/issues/87171) + app.on('browser-window-blur', () => { + if (!this.shuttingDown) { + this.saveWindowsState(); + } + }); + + // Handle various lifecycle events around windows + this.lifecycleMainService.onBeforeWindowClose(window => this.onBeforeWindowClose(window)); + this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown()); + this.windowsMainService.onWindowsCountChanged(e => { + if (e.newCount - e.oldCount > 0) { + // clear last closed window state when a new window opens. this helps on macOS where + // otherwise closing the last window, opening a new window and then quitting would + // use the state of the previously closed window when restarting. + this.lastClosedState = undefined; + } + }); + + // try to save state before destroy because close will not fire + this.windowsMainService.onWindowDestroyed(window => this.onBeforeWindowClose(window)); + } + + // Note that onBeforeShutdown() and onBeforeWindowClose() are fired in different order depending on the OS: + // - macOS: since the app will not quit when closing the last window, you will always first get + // the onBeforeShutdown() event followed by N onBeforeWindowClose() events for each window + // - other: on other OS, closing the last window will quit the app so the order depends on the + // user interaction: closing the last window will first trigger onBeforeWindowClose() + // and then onBeforeShutdown(). Using the quit action however will first issue onBeforeShutdown() + // and then onBeforeWindowClose(). + // + // Here is the behavior on different OS depending on action taken (Electron 1.7.x): + // + // Legend + // - quit(N): quit application with N windows opened + // - close(1): close one window via the window close button + // - closeAll: close all windows via the taskbar command + // - onBeforeShutdown(N): number of windows reported in this event handler + // - onBeforeWindowClose(N, M): number of windows reported and quitRequested boolean in this event handler + // + // macOS + // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) + // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) + // - quit(0): onBeforeShutdown(0) + // - close(1): onBeforeWindowClose(1, false) + // + // Windows + // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) + // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) + // - close(1): onBeforeWindowClose(2, false)[not last window] + // - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window] + // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) + // + // Linux + // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) + // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) + // - close(1): onBeforeWindowClose(2, false)[not last window] + // - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window] + // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) + // + private onBeforeShutdown(): void { + this.shuttingDown = true; + + this.saveWindowsState(); + } + + private saveWindowsState(): void { + const currentWindowsState: IWindowsState = { + openedWindows: [], + lastPluginDevelopmentHostWindow: this._state.lastPluginDevelopmentHostWindow, + lastActiveWindow: this.lastClosedState + }; + + // 1.) Find a last active window (pick any other first window otherwise) + if (!currentWindowsState.lastActiveWindow) { + let activeWindow = this.windowsMainService.getLastActiveWindow(); + if (!activeWindow || activeWindow.isExtensionDevelopmentHost) { + activeWindow = this.windowsMainService.getWindows().find(window => !window.isExtensionDevelopmentHost); + } + + if (activeWindow) { + currentWindowsState.lastActiveWindow = this.toWindowState(activeWindow); + } + } + + // 2.) Find extension host window + const extensionHostWindow = this.windowsMainService.getWindows().find(window => window.isExtensionDevelopmentHost && !window.isExtensionTestHost); + if (extensionHostWindow) { + currentWindowsState.lastPluginDevelopmentHostWindow = this.toWindowState(extensionHostWindow); + } + + // 3.) All windows (except extension host) for N >= 2 to support `restoreWindows: all` or for auto update + // + // Careful here: asking a window for its window state after it has been closed returns bogus values (width: 0, height: 0) + // so if we ever want to persist the UI state of the last closed window (window count === 1), it has + // to come from the stored lastClosedWindowState on Win/Linux at least + if (this.windowsMainService.getWindowCount() > 1) { + currentWindowsState.openedWindows = this.windowsMainService.getWindows().filter(window => !window.isExtensionDevelopmentHost).map(window => this.toWindowState(window)); + } + + // Persist + const state = getWindowsStateStoreData(currentWindowsState); + this.stateService.setItem(WindowsStateHandler.windowsStateStorageKey, state); + + if (this.shuttingDown) { + this.logService.trace('[WindowsStateHandler] onBeforeShutdown', state); + } + } + + // See note on #onBeforeShutdown() for details how these events are flowing + private onBeforeWindowClose(window: ICodeWindow): void { + if (this.lifecycleMainService.quitRequested) { + return; // during quit, many windows close in parallel so let it be handled in the before-quit handler + } + + // On Window close, update our stored UI state of this window + const state: IWindowState = this.toWindowState(window); + if (window.isExtensionDevelopmentHost && !window.isExtensionTestHost) { + this._state.lastPluginDevelopmentHostWindow = state; // do not let test run window state overwrite our extension development state + } + + // Any non extension host window with same workspace or folder + else if (!window.isExtensionDevelopmentHost && window.openedWorkspace) { + this._state.openedWindows.forEach(openedWindow => { + const sameWorkspace = isWorkspaceIdentifier(window.openedWorkspace) && openedWindow.workspace?.id === window.openedWorkspace.id; + const sameFolder = isSingleFolderWorkspaceIdentifier(window.openedWorkspace) && openedWindow.folderUri && extUriBiasedIgnorePathCase.isEqual(openedWindow.folderUri, window.openedWorkspace.uri); + + if (sameWorkspace || sameFolder) { + openedWindow.uiState = state.uiState; + } + }); + } + + // On Windows and Linux closing the last window will trigger quit. Since we are storing all UI state + // before quitting, we need to remember the UI state of this window to be able to persist it. + // On macOS we keep the last closed window state ready in case the user wants to quit right after or + // wants to open another window, in which case we use this state over the persisted one. + if (this.windowsMainService.getWindowCount() === 1) { + this.lastClosedState = state; + } + } + + private toWindowState(window: ICodeWindow): IWindowState { + return { + workspace: isWorkspaceIdentifier(window.openedWorkspace) ? window.openedWorkspace : undefined, + folderUri: isSingleFolderWorkspaceIdentifier(window.openedWorkspace) ? window.openedWorkspace.uri : undefined, + backupPath: window.backupPath, + remoteAuthority: window.remoteAuthority, + uiState: window.serializeWindowState() + }; + } + + getNewWindowState(configuration: INativeWindowConfiguration): INewWindowState { + const state = this.doGetNewWindowState(configuration); + const windowConfig = this.configurationService.getValue('window'); + + // Window state is not from a previous session: only allow fullscreen if we inherit it or user wants fullscreen + let allowFullscreen: boolean; + if (state.hasDefaultState) { + allowFullscreen = !!(windowConfig?.newWindowDimensions && ['fullscreen', 'inherit', 'offset'].indexOf(windowConfig.newWindowDimensions) >= 0); + } + + // Window state is from a previous session: only allow fullscreen when we got updated or user wants to restore + else { + allowFullscreen = !!(this.lifecycleMainService.wasRestarted || windowConfig?.restoreFullscreen); + + if (allowFullscreen && isMacintosh && this.windowsMainService.getWindows().some(window => window.isFullScreen)) { + // macOS: Electron does not allow to restore multiple windows in + // fullscreen. As such, if we already restored a window in that + // state, we cannot allow more fullscreen windows. See + // https://github.com/microsoft/vscode/issues/41691 and + // https://github.com/electron/electron/issues/13077 + allowFullscreen = false; + } + } + + if (state.mode === WindowMode.Fullscreen && !allowFullscreen) { + state.mode = WindowMode.Normal; + } + + return state; + } + + private doGetNewWindowState(configuration: INativeWindowConfiguration): INewWindowState { + const lastActive = this.windowsMainService.getLastActiveWindow(); + + // Restore state unless we are running extension tests + if (!configuration.extensionTestsPath) { + + // extension development host Window - load from stored settings if any + if (!!configuration.extensionDevelopmentPath && this.state.lastPluginDevelopmentHostWindow) { + return this.state.lastPluginDevelopmentHostWindow.uiState; + } + + // Known Workspace - load from stored settings + const workspace = configuration.workspace; + if (isWorkspaceIdentifier(workspace)) { + const stateForWorkspace = this.state.openedWindows.filter(openedWindow => openedWindow.workspace && openedWindow.workspace.id === workspace.id).map(openedWindow => openedWindow.uiState); + if (stateForWorkspace.length) { + return stateForWorkspace[0]; + } + } + + // Known Folder - load from stored settings + if (isSingleFolderWorkspaceIdentifier(workspace)) { + const stateForFolder = this.state.openedWindows.filter(openedWindow => openedWindow.folderUri && extUriBiasedIgnorePathCase.isEqual(openedWindow.folderUri, workspace.uri)).map(openedWindow => openedWindow.uiState); + if (stateForFolder.length) { + return stateForFolder[0]; + } + } + + // Empty windows with backups + else if (configuration.backupPath) { + const stateForEmptyWindow = this.state.openedWindows.filter(openedWindow => openedWindow.backupPath === configuration.backupPath).map(openedWindow => openedWindow.uiState); + if (stateForEmptyWindow.length) { + return stateForEmptyWindow[0]; + } + } + + // First Window + const lastActiveState = this.lastClosedState || this.state.lastActiveWindow; + if (!lastActive && lastActiveState) { + return lastActiveState.uiState; + } + } + + // + // In any other case, we do not have any stored settings for the window state, so we come up with something smart + // + + // We want the new window to open on the same display that the last active one is in + let displayToUse: Display | undefined; + const displays = screen.getAllDisplays(); + + // Single Display + if (displays.length === 1) { + displayToUse = displays[0]; + } + + // Multi Display + else { + + // on mac there is 1 menu per window so we need to use the monitor where the cursor currently is + if (isMacintosh) { + const cursorPoint = screen.getCursorScreenPoint(); + displayToUse = screen.getDisplayNearestPoint(cursorPoint); + } + + // if we have a last active window, use that display for the new window + if (!displayToUse && lastActive) { + displayToUse = screen.getDisplayMatching(lastActive.getBounds()); + } + + // fallback to primary display or first display + if (!displayToUse) { + displayToUse = screen.getPrimaryDisplay() || displays[0]; + } + } + + // Compute x/y based on display bounds + // Note: important to use Math.round() because Electron does not seem to be too happy about + // display coordinates that are not absolute numbers. + let state = defaultWindowState(); + state.x = Math.round(displayToUse.bounds.x + (displayToUse.bounds.width / 2) - (state.width! / 2)); + state.y = Math.round(displayToUse.bounds.y + (displayToUse.bounds.height / 2) - (state.height! / 2)); + + // Check for newWindowDimensions setting and adjust accordingly + const windowConfig = this.configurationService.getValue('window'); + let ensureNoOverlap = true; + if (windowConfig?.newWindowDimensions) { + if (windowConfig.newWindowDimensions === 'maximized') { + state.mode = WindowMode.Maximized; + ensureNoOverlap = false; + } else if (windowConfig.newWindowDimensions === 'fullscreen') { + state.mode = WindowMode.Fullscreen; + ensureNoOverlap = false; + } else if ((windowConfig.newWindowDimensions === 'inherit' || windowConfig.newWindowDimensions === 'offset') && lastActive) { + const lastActiveState = lastActive.serializeWindowState(); + if (lastActiveState.mode === WindowMode.Fullscreen) { + state.mode = WindowMode.Fullscreen; // only take mode (fixes https://github.com/microsoft/vscode/issues/19331) + } else { + state = lastActiveState; + } + + ensureNoOverlap = state.mode !== WindowMode.Fullscreen && windowConfig.newWindowDimensions === 'offset'; + } + } + + if (ensureNoOverlap) { + state = this.ensureNoOverlap(state); + } + + (state as INewWindowState).hasDefaultState = true; // flag as default state + + return state; + } + + private ensureNoOverlap(state: IWindowUIState): IWindowUIState { + if (this.windowsMainService.getWindows().length === 0) { + return state; + } + + state.x = typeof state.x === 'number' ? state.x : 0; + state.y = typeof state.y === 'number' ? state.y : 0; + + const existingWindowBounds = this.windowsMainService.getWindows().map(window => window.getBounds()); + while (existingWindowBounds.some(bounds => bounds.x === state.x || bounds.y === state.y)) { + state.x += 30; + state.y += 30; + } + + return state; + } +} + +export function restoreWindowsState(data: ISerializedWindowsState | undefined): IWindowsState { + const result: IWindowsState = { openedWindows: [] }; + const windowsState = data || { openedWindows: [] }; + + if (windowsState.lastActiveWindow) { + result.lastActiveWindow = restoreWindowState(windowsState.lastActiveWindow); + } + + if (windowsState.lastPluginDevelopmentHostWindow) { + result.lastPluginDevelopmentHostWindow = restoreWindowState(windowsState.lastPluginDevelopmentHostWindow); + } + + if (Array.isArray(windowsState.openedWindows)) { + result.openedWindows = windowsState.openedWindows.map(windowState => restoreWindowState(windowState)); + } + + return result; +} + +function restoreWindowState(windowState: ISerializedWindowState): IWindowState { + const result: IWindowState = { uiState: windowState.uiState }; + if (windowState.backupPath) { + result.backupPath = windowState.backupPath; + } + + if (windowState.remoteAuthority) { + result.remoteAuthority = windowState.remoteAuthority; + } + + if (windowState.folder) { + result.folderUri = URI.parse(windowState.folder); + } + + if (windowState.workspaceIdentifier) { + result.workspace = { id: windowState.workspaceIdentifier.id, configPath: URI.parse(windowState.workspaceIdentifier.configURIPath) }; + } + + return result; +} + +export function getWindowsStateStoreData(windowsState: IWindowsState): IWindowsState { + return { + lastActiveWindow: windowsState.lastActiveWindow && serializeWindowState(windowsState.lastActiveWindow), + lastPluginDevelopmentHostWindow: windowsState.lastPluginDevelopmentHostWindow && serializeWindowState(windowsState.lastPluginDevelopmentHostWindow), + openedWindows: windowsState.openedWindows.map(ws => serializeWindowState(ws)) + }; +} + +function serializeWindowState(windowState: IWindowState): ISerializedWindowState { + return { + workspaceIdentifier: windowState.workspace && { id: windowState.workspace.id, configURIPath: windowState.workspace.configPath.toString() }, + folder: windowState.folderUri && windowState.folderUri.toString(), + backupPath: windowState.backupPath, + remoteAuthority: windowState.remoteAuthority, + uiState: windowState.uiState + }; +} diff --git a/src/vs/platform/windows/electron-main/windowsStateStorage.ts b/src/vs/platform/windows/electron-main/windowsStateStorage.ts deleted file mode 100644 index 165333950..000000000 --- a/src/vs/platform/windows/electron-main/windowsStateStorage.ts +++ /dev/null @@ -1,93 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI, UriComponents } from 'vs/base/common/uri'; -import { IWindowState as IWindowUIState } from 'vs/platform/windows/electron-main/windows'; -import { IWindowState, IWindowsState } from 'vs/platform/windows/electron-main/windowsMainService'; - -export type WindowsStateStorageData = object; - -interface ISerializedWindowsState { - lastActiveWindow?: ISerializedWindowState; - lastPluginDevelopmentHostWindow?: ISerializedWindowState; - openedWindows: ISerializedWindowState[]; -} - -interface ISerializedWindowState { - workspaceIdentifier?: { id: string; configURIPath: string }; - folder?: string; - backupPath?: string; - remoteAuthority?: string; - uiState: IWindowUIState; - - // deprecated - folderUri?: UriComponents; - folderPath?: string; - workspace?: { id: string; configPath: string }; -} - -export function restoreWindowsState(data: WindowsStateStorageData | undefined): IWindowsState { - const result: IWindowsState = { openedWindows: [] }; - const windowsState = data as ISerializedWindowsState || { openedWindows: [] }; - - if (windowsState.lastActiveWindow) { - result.lastActiveWindow = restoreWindowState(windowsState.lastActiveWindow); - } - - if (windowsState.lastPluginDevelopmentHostWindow) { - result.lastPluginDevelopmentHostWindow = restoreWindowState(windowsState.lastPluginDevelopmentHostWindow); - } - - if (Array.isArray(windowsState.openedWindows)) { - result.openedWindows = windowsState.openedWindows.map(windowState => restoreWindowState(windowState)); - } - - return result; -} - -function restoreWindowState(windowState: ISerializedWindowState): IWindowState { - const result: IWindowState = { uiState: windowState.uiState }; - if (windowState.backupPath) { - result.backupPath = windowState.backupPath; - } - - if (windowState.remoteAuthority) { - result.remoteAuthority = windowState.remoteAuthority; - } - - if (windowState.folder) { - result.folderUri = URI.parse(windowState.folder); - } else if (windowState.folderUri) { - result.folderUri = URI.revive(windowState.folderUri); - } else if (windowState.folderPath) { - result.folderUri = URI.file(windowState.folderPath); - } - - if (windowState.workspaceIdentifier) { - result.workspace = { id: windowState.workspaceIdentifier.id, configPath: URI.parse(windowState.workspaceIdentifier.configURIPath) }; - } else if (windowState.workspace) { - result.workspace = { id: windowState.workspace.id, configPath: URI.file(windowState.workspace.configPath) }; - } - - return result; -} - -export function getWindowsStateStoreData(windowsState: IWindowsState): WindowsStateStorageData { - return { - lastActiveWindow: windowsState.lastActiveWindow && serializeWindowState(windowsState.lastActiveWindow), - lastPluginDevelopmentHostWindow: windowsState.lastPluginDevelopmentHostWindow && serializeWindowState(windowsState.lastPluginDevelopmentHostWindow), - openedWindows: windowsState.openedWindows.map(ws => serializeWindowState(ws)) - }; -} - -function serializeWindowState(windowState: IWindowState): ISerializedWindowState { - return { - workspaceIdentifier: windowState.workspace && { id: windowState.workspace.id, configURIPath: windowState.workspace.configPath.toString() }, - folder: windowState.folderUri && windowState.folderUri.toString(), - backupPath: windowState.backupPath, - remoteAuthority: windowState.remoteAuthority, - uiState: windowState.uiState - }; -} diff --git a/src/vs/platform/windows/node/window.ts b/src/vs/platform/windows/node/window.ts deleted file mode 100644 index 6859b36ab..000000000 --- a/src/vs/platform/windows/node/window.ts +++ /dev/null @@ -1,150 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI } from 'vs/base/common/uri'; -import * as platform from 'vs/base/common/platform'; -import * as extpath from 'vs/base/common/extpath'; -import { IWorkspaceIdentifier, IResolvedWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; - -export const enum OpenContext { - - // opening when running from the command line - CLI, - - // macOS only: opening from the dock (also when opening files to a running instance from desktop) - DOCK, - - // opening from the main application window - MENU, - - // opening from a file or folder dialog - DIALOG, - - // opening from the OS's UI - DESKTOP, - - // opening through the API - API -} - -export interface IWindowContext { - openedWorkspace?: IWorkspaceIdentifier; - openedFolderUri?: URI; - - extensionDevelopmentPath?: string[]; - lastFocusTime: number; -} - -export interface IBestWindowOrFolderOptions { - windows: W[]; - newWindow: boolean; - context: OpenContext; - fileUri?: URI; - codeSettingsFolder?: string; - localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null; -} - -export function findBestWindowOrFolderForFile({ windows, newWindow, context, fileUri, localWorkspaceResolver: workspaceResolver }: IBestWindowOrFolderOptions): W | undefined { - if (!newWindow && fileUri && (context === OpenContext.DESKTOP || context === OpenContext.CLI || context === OpenContext.DOCK)) { - const windowOnFilePath = findWindowOnFilePath(windows, fileUri, workspaceResolver); - if (windowOnFilePath) { - return windowOnFilePath; - } - } - return !newWindow ? getLastActiveWindow(windows) : undefined; -} - -function findWindowOnFilePath(windows: W[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null): W | null { - - // First check for windows with workspaces that have a parent folder of the provided path opened - for (const window of windows) { - const workspace = window.openedWorkspace; - if (workspace) { - const resolvedWorkspace = localWorkspaceResolver(workspace); - if (resolvedWorkspace) { - // workspace could be resolved: It's in the local file system - if (resolvedWorkspace.folders.some(folder => extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, folder.uri))) { - return window; - } - } else { - // use the config path instead - if (extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, workspace.configPath)) { - return window; - } - } - } - } - - // Then go with single folder windows that are parent of the provided file path - const singleFolderWindowsOnFilePath = windows.filter(window => window.openedFolderUri && extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, window.openedFolderUri)); - if (singleFolderWindowsOnFilePath.length) { - return singleFolderWindowsOnFilePath.sort((a, b) => -(a.openedFolderUri!.path.length - b.openedFolderUri!.path.length))[0]; - } - - return null; -} - -export function getLastActiveWindow(windows: W[]): W | undefined { - const lastFocusedDate = Math.max.apply(Math, windows.map(window => window.lastFocusTime)); - - return windows.find(window => window.lastFocusTime === lastFocusedDate); -} - -export function findWindowOnWorkspace(windows: W[], workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)): W | null { - if (isSingleFolderWorkspaceIdentifier(workspace)) { - for (const window of windows) { - // match on folder - if (isSingleFolderWorkspaceIdentifier(workspace)) { - if (window.openedFolderUri && extUriBiasedIgnorePathCase.isEqual(window.openedFolderUri, workspace)) { - return window; - } - } - } - } else if (isWorkspaceIdentifier(workspace)) { - for (const window of windows) { - // match on workspace - if (window.openedWorkspace && window.openedWorkspace.id === workspace.id) { - return window; - } - } - } - return null; -} - -export function findWindowOnExtensionDevelopmentPath(windows: W[], extensionDevelopmentPaths: string[]): W | null { - - const matches = (uriString: string): boolean => { - return extensionDevelopmentPaths.some(p => extpath.isEqual(p, uriString, !platform.isLinux /* ignorecase */)); - }; - - for (const window of windows) { - // match on extension development path. The path can be one or more paths or uri strings, using paths.isEqual is not 100% correct but good enough - const currPaths = window.extensionDevelopmentPath; - if (currPaths?.some(p => matches(p))) { - return window; - } - } - - return null; -} - -export function findWindowOnWorkspaceOrFolderUri(windows: W[], uri: URI | undefined): W | null { - if (!uri) { - return null; - } - for (const window of windows) { - // check for workspace config path - if (window.openedWorkspace && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, uri)) { - return window; - } - - // check for folder path - if (window.openedFolderUri && extUriBiasedIgnorePathCase.isEqual(window.openedFolderUri, uri)) { - return window; - } - } - return null; -} diff --git a/src/vs/platform/windows/common/windowTracker.ts b/src/vs/platform/windows/node/windowTracker.ts similarity index 100% rename from src/vs/platform/windows/common/windowTracker.ts rename to src/vs/platform/windows/node/windowTracker.ts diff --git a/src/vs/platform/windows/test/electron-main/window.test.ts b/src/vs/platform/windows/test/electron-main/window.test.ts new file mode 100644 index 000000000..42f458f97 --- /dev/null +++ b/src/vs/platform/windows/test/electron-main/window.test.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as path from 'vs/base/common/path'; +import { findWindowOnFile } from 'vs/platform/windows/electron-main/windowsFinder'; +import { ICodeWindow, IWindowState } from 'vs/platform/windows/electron-main/windows'; +import { IWorkspaceIdentifier, toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; +import { URI } from 'vs/base/common/uri'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Event } from 'vs/base/common/event'; +import { UriDto } from 'vs/base/common/types'; +import { ICommandAction } from 'vs/platform/actions/common/actions'; +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; + +const fixturesFolder = getPathFromAmdModule(require, './fixtures'); + +const testWorkspace: IWorkspaceIdentifier = { + id: Date.now().toString(), + configPath: URI.file(path.join(fixturesFolder, 'workspaces.json')) +}; + +const testWorkspaceFolders = toWorkspaceFolders([{ path: path.join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: path.join(fixturesFolder, 'vscode_workspace_2_folder') }], testWorkspace.configPath, extUriBiasedIgnorePathCase); +const localWorkspaceResolver = (workspace: any) => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: testWorkspaceFolders } : null; }; + +function createTestCodeWindow(options: { lastFocusTime: number, openedFolderUri?: URI, openedWorkspace?: IWorkspaceIdentifier }): ICodeWindow { + return new class implements ICodeWindow { + onLoad: Event = Event.None; + onReady: Event = Event.None; + onClose: Event = Event.None; + onDestroy: Event = Event.None; + whenClosedOrLoaded: Promise = Promise.resolve(); + id: number = -1; + win: Electron.BrowserWindow = undefined!; + config: INativeWindowConfiguration | undefined; + openedWorkspace = options.openedFolderUri ? { id: '', uri: options.openedFolderUri } : options.openedWorkspace; + backupPath?: string | undefined; + remoteAuthority?: string | undefined; + isExtensionDevelopmentHost = false; + isExtensionTestHost = false; + lastFocusTime = options.lastFocusTime; + isFullScreen = false; + isReady = true; + hasHiddenTitleBarStyle = false; + + ready(): Promise { throw new Error('Method not implemented.'); } + setReady(): void { throw new Error('Method not implemented.'); } + addTabbedWindow(window: ICodeWindow): void { throw new Error('Method not implemented.'); } + load(config: INativeWindowConfiguration, options: { isReload?: boolean }): void { throw new Error('Method not implemented.'); } + reload(cli?: NativeParsedArgs): void { throw new Error('Method not implemented.'); } + focus(options?: { force: boolean; }): void { throw new Error('Method not implemented.'); } + close(): void { throw new Error('Method not implemented.'); } + getBounds(): Electron.Rectangle { throw new Error('Method not implemented.'); } + send(channel: string, ...args: any[]): void { throw new Error('Method not implemented.'); } + sendWhenReady(channel: string, token: CancellationToken, ...args: any[]): void { throw new Error('Method not implemented.'); } + toggleFullScreen(): void { throw new Error('Method not implemented.'); } + isMinimized(): boolean { throw new Error('Method not implemented.'); } + setRepresentedFilename(name: string): void { throw new Error('Method not implemented.'); } + getRepresentedFilename(): string | undefined { throw new Error('Method not implemented.'); } + setDocumentEdited(edited: boolean): void { throw new Error('Method not implemented.'); } + isDocumentEdited(): boolean { throw new Error('Method not implemented.'); } + handleTitleDoubleClick(): void { throw new Error('Method not implemented.'); } + updateTouchBar(items: UriDto[][]): void { throw new Error('Method not implemented.'); } + serializeWindowState(): IWindowState { throw new Error('Method not implemented'); } + dispose(): void { } + }; +} + +const vscodeFolderWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder')) }); +const lastActiveWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 3, openedFolderUri: undefined }); +const noVscodeFolderWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }); +const windows: ICodeWindow[] = [ + vscodeFolderWindow, + lastActiveWindow, + noVscodeFolderWindow, +]; + +suite('WindowsFinder', () => { + + test('New window without folder when no windows exist', () => { + assert.strictEqual(findWindowOnFile([], URI.file('nonexisting'), localWorkspaceResolver), undefined); + assert.strictEqual(findWindowOnFile([], URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), localWorkspaceResolver), undefined); + }); + + test('Existing window with folder', () => { + assert.strictEqual(findWindowOnFile(windows, URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), localWorkspaceResolver), noVscodeFolderWindow); + + assert.strictEqual(findWindowOnFile(windows, URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), localWorkspaceResolver), vscodeFolderWindow); + + const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder')) }); + assert.strictEqual(findWindowOnFile([window], URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), window); + }); + + test('More specific existing window wins', () => { + const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }); + const nestedFolderWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder')) }); + assert.strictEqual(findWindowOnFile([window, nestedFolderWindow], URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), nestedFolderWindow); + }); + + test('Workspace folder wins', () => { + const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedWorkspace: testWorkspace }); + assert.strictEqual(findWindowOnFile([window], URI.file(path.join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), window); + }); +}); diff --git a/src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts similarity index 68% rename from src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts rename to src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts index 419caee0d..956ab942b 100644 --- a/src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts @@ -6,12 +6,10 @@ import * as assert from 'assert'; import * as os from 'os'; import * as path from 'vs/base/common/path'; - -import { restoreWindowsState, getWindowsStateStoreData } from 'vs/platform/windows/electron-main/windowsStateStorage'; +import { restoreWindowsState, getWindowsStateStoreData, IWindowsState, IWindowState } from 'vs/platform/windows/electron-main/windowsStateHandler'; import { IWindowState as IWindowUIState, WindowMode } from 'vs/platform/windows/electron-main/windows'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { URI } from 'vs/base/common/uri'; -import { IWindowsState, IWindowState } from 'vs/platform/windows/electron-main/windowsMainService'; function getUIState(): IWindowUIState { return { @@ -30,15 +28,15 @@ function toWorkspace(uri: URI): IWorkspaceIdentifier { }; } function assertEqualURI(u1: URI | undefined, u2: URI | undefined, message?: string): void { - assert.equal(u1 && u1.toString(), u2 && u2.toString(), message); + assert.strictEqual(u1 && u1.toString(), u2 && u2.toString(), message); } function assertEqualWorkspace(w1: IWorkspaceIdentifier | undefined, w2: IWorkspaceIdentifier | undefined, message?: string): void { if (!w1 || !w2) { - assert.equal(w1, w2, message); + assert.strictEqual(w1, w2, message); return; } - assert.equal(w1.id, w2.id, message); + assert.strictEqual(w1.id, w2.id, message); assertEqualURI(w1.configPath, w2.configPath, message); } @@ -47,9 +45,9 @@ function assertEqualWindowState(expected: IWindowState | undefined, actual: IWin assert.deepEqual(expected, actual, message); return; } - assert.equal(expected.backupPath, actual.backupPath, message); + assert.strictEqual(expected.backupPath, actual.backupPath, message); assertEqualURI(expected.folderUri, actual.folderUri, message); - assert.equal(expected.remoteAuthority, actual.remoteAuthority, message); + assert.strictEqual(expected.remoteAuthority, actual.remoteAuthority, message); assertEqualWorkspace(expected.workspace, actual.workspace, message); assert.deepEqual(expected.uiState, actual.uiState, message); } @@ -57,7 +55,7 @@ function assertEqualWindowState(expected: IWindowState | undefined, actual: IWin function assertEqualWindowsState(expected: IWindowsState, actual: IWindowsState, message?: string) { assertEqualWindowState(expected.lastPluginDevelopmentHostWindow, actual.lastPluginDevelopmentHostWindow, message); assertEqualWindowState(expected.lastActiveWindow, actual.lastActiveWindow, message); - assert.equal(expected.openedWindows.length, actual.openedWindows.length, message); + assert.strictEqual(expected.openedWindows.length, actual.openedWindows.length, message); for (let i = 0; i < expected.openedWindows.length; i++) { assertEqualWindowState(expected.openedWindows[i], actual.openedWindows[i], message); } @@ -117,94 +115,6 @@ suite('Windows State Storing', () => { assertRestoring(windowState, 'lastPluginDevelopmentHostWindow'); }); - test('open 1_31', () => { - const v1_31_workspace = `{ - "openedWindows": [], - "lastActiveWindow": { - "workspace": { - "id": "a41787288b5e9cc1a61ba2dd84cd0d80", - "configPath": "/home/user/workspaces/code-and-docs.code-workspace" - }, - "backupPath": "/home/user/.config/Code - Insiders/Backups/a41787288b5e9cc1a61ba2dd84cd0d80", - "uiState": { - "mode": 0, - "x": 0, - "y": 27, - "width": 2560, - "height": 1364 - } - } - }`; - - let windowsState = restoreWindowsState(JSON.parse(v1_31_workspace)); - let expected: IWindowsState = { - openedWindows: [], - lastActiveWindow: { - backupPath: '/home/user/.config/Code - Insiders/Backups/a41787288b5e9cc1a61ba2dd84cd0d80', - uiState: { mode: WindowMode.Maximized, x: 0, y: 27, width: 2560, height: 1364 }, - workspace: { id: 'a41787288b5e9cc1a61ba2dd84cd0d80', configPath: URI.file('/home/user/workspaces/code-and-docs.code-workspace') } - } - }; - - assertEqualWindowsState(expected, windowsState, 'v1_31_workspace'); - - const v1_31_folder = `{ - "openedWindows": [], - "lastPluginDevelopmentHostWindow": { - "folderUri": { - "$mid": 1, - "fsPath": "/home/user/workspaces/testing/customdata", - "external": "file:///home/user/workspaces/testing/customdata", - "path": "/home/user/workspaces/testing/customdata", - "scheme": "file" - }, - "uiState": { - "mode": 1, - "x": 593, - "y": 617, - "width": 1625, - "height": 595 - } - } - }`; - - windowsState = restoreWindowsState(JSON.parse(v1_31_folder)); - expected = { - openedWindows: [], - lastPluginDevelopmentHostWindow: { - uiState: { mode: WindowMode.Normal, x: 593, y: 617, width: 1625, height: 595 }, - folderUri: URI.parse('file:///home/user/workspaces/testing/customdata') - } - }; - assertEqualWindowsState(expected, windowsState, 'v1_31_folder'); - - const v1_31_empty_window = ` { - "openedWindows": [ - ], - "lastActiveWindow": { - "backupPath": "C:\\\\Users\\\\Mike\\\\AppData\\\\Roaming\\\\Code\\\\Backups\\\\1549538599815", - "uiState": { - "mode": 0, - "x": -8, - "y": -8, - "width": 2576, - "height": 1344 - } - } - }`; - - windowsState = restoreWindowsState(JSON.parse(v1_31_empty_window)); - expected = { - openedWindows: [], - lastActiveWindow: { - backupPath: 'C:\\Users\\Mike\\AppData\\Roaming\\Code\\Backups\\1549538599815', - uiState: { mode: WindowMode.Maximized, x: -8, y: -8, width: 2576, height: 1344 } - } - }; - assertEqualWindowsState(expected, windowsState, 'v1_31_empty_window'); - - }); - test('open 1_32', () => { const v1_32_workspace = `{ "openedWindows": [], @@ -286,7 +196,5 @@ suite('Windows State Storing', () => { } }; assertEqualWindowsState(expected, windowsState, 'v1_32_empty_window'); - }); - }); diff --git a/src/vs/platform/windows/test/node/window.test.ts b/src/vs/platform/windows/test/node/window.test.ts deleted file mode 100644 index 576d30d13..000000000 --- a/src/vs/platform/windows/test/node/window.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import * as path from 'vs/base/common/path'; -import { IBestWindowOrFolderOptions, IWindowContext, findBestWindowOrFolderForFile, OpenContext } from 'vs/platform/windows/node/window'; -import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; -import { URI } from 'vs/base/common/uri'; -import { getPathFromAmdModule } from 'vs/base/common/amd'; - -const fixturesFolder = getPathFromAmdModule(require, './fixtures'); - -const testWorkspace: IWorkspaceIdentifier = { - id: Date.now().toString(), - configPath: URI.file(path.join(fixturesFolder, 'workspaces.json')) -}; - -const testWorkspaceFolders = toWorkspaceFolders([{ path: path.join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: path.join(fixturesFolder, 'vscode_workspace_2_folder') }], testWorkspace.configPath); - -function options(custom?: Partial>): IBestWindowOrFolderOptions { - return { - windows: [], - newWindow: false, - context: OpenContext.CLI, - codeSettingsFolder: '_vscode', - localWorkspaceResolver: workspace => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: testWorkspaceFolders } : null; }, - ...custom - }; -} - -const vscodeFolderWindow: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder')) }; -const lastActiveWindow: IWindowContext = { lastFocusTime: 3, openedFolderUri: undefined }; -const noVscodeFolderWindow: IWindowContext = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }; -const windows: IWindowContext[] = [ - vscodeFolderWindow, - lastActiveWindow, - noVscodeFolderWindow, -]; - -suite('WindowsFinder', () => { - - test('New window without folder when no windows exist', () => { - assert.equal(findBestWindowOrFolderForFile(options()), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')) - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - newWindow: true - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - context: OpenContext.API - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')) - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'new_folder', 'new_file.txt')) - })), null); - }); - - test('New window without folder when windows exist', () => { - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), - newWindow: true - })), null); - }); - - test('Last active window', () => { - assert.equal(findBestWindowOrFolderForFile(options({ - windows - })), lastActiveWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder2', 'file.txt')) - })), lastActiveWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows: [lastActiveWindow, noVscodeFolderWindow], - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - })), lastActiveWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), - context: OpenContext.API - })), lastActiveWindow); - }); - - test('Existing window with folder', () => { - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')) - })), noVscodeFolderWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')) - })), vscodeFolderWindow); - const window: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder')) }; - assert.equal(findBestWindowOrFolderForFile(options({ - windows: [window], - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt')) - })), window); - }); - - test('More specific existing window wins', () => { - const window: IWindowContext = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }; - const nestedFolderWindow: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder')) }; - assert.equal(findBestWindowOrFolderForFile(options({ - windows: [window, nestedFolderWindow], - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt')) - })), nestedFolderWindow); - }); - - test('Workspace folder wins', () => { - const window: IWindowContext = { lastFocusTime: 1, openedWorkspace: testWorkspace }; - assert.equal(findBestWindowOrFolderForFile(options({ - windows: [window], - fileUri: URI.file(path.join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt')) - })), window); - }); -}); diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 72ca615f9..38a11c92f 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -4,16 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import * as resources from 'vs/base/common/resources'; +import { joinPath, basenameOrAuthority } from 'vs/base/common/resources'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TernarySearchTree } from 'vs/base/common/map'; import { Event } from 'vs/base/common/event'; -import { IWorkspaceIdentifier, IStoredWorkspaceFolder, isRawFileWorkspaceFolder, isRawUriWorkspaceFolder, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IStoredWorkspaceFolder, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspaceFolderProvider } from 'vs/base/common/labels'; export const IWorkspaceContextService = createDecorator('contextService'); export interface IWorkspaceContextService extends IWorkspaceFolderProvider { + readonly _serviceBrand: undefined; /** @@ -58,9 +59,9 @@ export interface IWorkspaceContextService extends IWorkspaceFolderProvider { getWorkspaceFolder(resource: URI): IWorkspaceFolder | null; /** - * Return `true` if the current workspace has the given identifier otherwise `false`. + * Return `true` if the current workspace has the given identifier or root URI otherwise `false`. */ - isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean; + isCurrentWorkspace(workspaceIdOrFolder: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI): boolean; /** * Returns if the provided resource is inside the workspace or not. @@ -80,14 +81,6 @@ export interface IWorkspaceFoldersChangeEvent { changed: IWorkspaceFolder[]; } -export namespace IWorkspace { - export function isIWorkspace(thing: unknown): thing is IWorkspace { - return !!(thing && typeof thing === 'object' - && typeof (thing as IWorkspace).id === 'string' - && Array.isArray((thing as IWorkspace).folders)); - } -} - export interface IWorkspace { /** @@ -106,6 +99,14 @@ export interface IWorkspace { readonly configuration?: URI | null; } +export function isWorkspace(thing: unknown): thing is IWorkspace { + const candidate = thing as IWorkspace | undefined; + + return !!(candidate && typeof candidate === 'object' + && typeof candidate.id === 'string' + && Array.isArray(candidate.folders)); +} + export interface IWorkspaceFolderData { /** @@ -125,15 +126,6 @@ export interface IWorkspaceFolderData { readonly index: number; } -export namespace IWorkspaceFolder { - export function isIWorkspaceFolder(thing: unknown): thing is IWorkspaceFolder { - return !!(thing && typeof thing === 'object' - && URI.isUri((thing as IWorkspaceFolder).uri) - && typeof (thing as IWorkspaceFolder).name === 'string' - && typeof (thing as IWorkspaceFolder).toResource === 'function'); - } -} - export interface IWorkspaceFolder extends IWorkspaceFolderData { /** @@ -142,6 +134,15 @@ export interface IWorkspaceFolder extends IWorkspaceFolderData { toResource: (relativePath: string) => URI; } +export function isWorkspaceFolder(thing: unknown): thing is IWorkspaceFolder { + const candidate = thing as IWorkspaceFolder; + + return !!(candidate && typeof candidate === 'object' + && URI.isUri(candidate.uri) + && typeof candidate.name === 'string' + && typeof candidate.toResource === 'function'); +} + export class Workspace implements IWorkspace { private _foldersMap: TernarySearchTree = TernarySearchTree.forUris(this._ignorePathCasing); @@ -222,7 +223,7 @@ export class WorkspaceFolder implements IWorkspaceFolder { } toResource(relativePath: string): URI { - return resources.joinPath(this.uri, relativePath); + return joinPath(this.uri, relativePath); } toJSON(): IWorkspaceFolderData { @@ -231,43 +232,5 @@ export class WorkspaceFolder implements IWorkspaceFolder { } export function toWorkspaceFolder(resource: URI): WorkspaceFolder { - return new WorkspaceFolder({ uri: resource, index: 0, name: resources.basenameOrAuthority(resource) }, { uri: resource.toString() }); -} - -export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], workspaceConfigFile: URI): WorkspaceFolder[] { - let result: WorkspaceFolder[] = []; - let seen: Set = new Set(); - - const relativeTo = resources.dirname(workspaceConfigFile); - for (let configuredFolder of configuredFolders) { - let uri: URI | null = null; - if (isRawFileWorkspaceFolder(configuredFolder)) { - if (configuredFolder.path) { - uri = resources.resolvePath(relativeTo, configuredFolder.path); - } - } else if (isRawUriWorkspaceFolder(configuredFolder)) { - try { - uri = URI.parse(configuredFolder.uri); - // this makes sure all workspace folder are absolute - if (uri.path[0] !== '/') { - uri = uri.with({ path: '/' + uri.path }); - } - } catch (e) { - console.warn(e); - // ignore - } - } - if (uri) { - // remove duplicates - let comparisonKey = resources.getComparisonKey(uri); - if (!seen.has(comparisonKey)) { - seen.add(comparisonKey); - - const name = configuredFolder.name || resources.basenameOrAuthority(uri); - result.push(new WorkspaceFolder({ uri, name, index: result.length }, configuredFolder)); - } - } - } - - return result; + return new WorkspaceFolder({ uri: resource, index: 0, name: basenameOrAuthority(resource) }, { uri: resource.toString() }); } diff --git a/src/vs/platform/workspace/test/common/workspace.test.ts b/src/vs/platform/workspace/test/common/workspace.test.ts index 1d3f78f7b..9b26cb220 100644 --- a/src/vs/platform/workspace/test/common/workspace.test.ts +++ b/src/vs/platform/workspace/test/common/workspace.test.ts @@ -5,10 +5,11 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; -import { Workspace, toWorkspaceFolders, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; -import { IRawFileWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; +import { IRawFileWorkspaceFolder, toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; import { isLinux, isWindows } from 'vs/base/common/platform'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; suite('Workspace', () => { @@ -70,7 +71,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with single absolute folder', () => { - const actual = toWorkspaceFolders([{ path: '/src/test' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 1); assert.equal(actual[0].uri.fsPath, testFolderUri.fsPath); @@ -80,7 +81,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with single relative folder', () => { - const actual = toWorkspaceFolders([{ path: './test' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: './test' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 1); assert.equal(actual[0].uri.fsPath, testFolderUri.fsPath); @@ -90,7 +91,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with single absolute folder with name', () => { - const actual = toWorkspaceFolders([{ path: '/src/test', name: 'hello' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test', name: 'hello' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 1); @@ -101,7 +102,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple unique absolute folders', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3' }, { path: '/src/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3' }, { path: '/src/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -121,7 +122,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple unique absolute folders with names', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -141,7 +142,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple unique absolute and relative folders', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/abc/test3', name: 'noName' }, { path: './test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/abc/test3', name: 'noName' }, { path: './test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -161,7 +162,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple absolute folders with duplicates', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test2', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test2', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 2); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -176,7 +177,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple absolute and relative folders with duplicates', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -196,7 +197,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple absolute and relative folders with invalid paths', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index d6643801a..66413f2e4 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -5,11 +5,11 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { localize } from 'vs/nls'; -import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceFolder, IWorkspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { URI, UriComponents } from 'vs/base/common/uri'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; import { extname, isAbsolute } from 'vs/base/common/path'; -import { dirname, resolvePath, isEqualAuthority, relativePath, extname as resourceExtname, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { extname as resourceExtname, extUriBiasedIgnorePathCase, IExtUri } from 'vs/base/common/resources'; import * as jsonEdit from 'vs/base/common/jsonEdit'; import * as json from 'vs/base/common/json'; import { Schemas } from 'vs/base/common/network'; @@ -22,9 +22,16 @@ import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; export const WORKSPACE_EXTENSION = 'code-workspace'; +const WORKSPACE_SUFFIX = `.${WORKSPACE_EXTENSION}`; export const WORKSPACE_FILTER = [{ name: localize('codeWorkspace', "Code Workspace"), extensions: [WORKSPACE_EXTENSION] }]; export const UNTITLED_WORKSPACE_NAME = 'workspace.json'; +export function hasWorkspaceFileExtension(path: string | URI) { + const ext = (typeof path === 'string') ? extname(path) : resourceExtname(path); + + return ext === WORKSPACE_SUFFIX; +} + export const IWorkspacesService = createDecorator('workspacesService'); export interface IWorkspacesService { @@ -48,6 +55,8 @@ export interface IWorkspacesService { getDirtyWorkspaces(): Promise>; } +//#region Workspaces Recently Opened + export interface IRecentlyOpened { workspaces: Array; files: IRecentFile[]; @@ -61,7 +70,7 @@ export interface IRecentWorkspace { } export interface IRecentFolder { - folderUri: ISingleFolderWorkspaceIdentifier; + folderUri: URI; label?: string; } @@ -82,36 +91,114 @@ export function isRecentFile(curr: IRecent): curr is IRecentFile { return curr.hasOwnProperty('fileUri'); } -/** - * A single folder workspace identifier is just the path to the folder. - */ -export type ISingleFolderWorkspaceIdentifier = URI; +//#endregion -export interface IWorkspaceIdentifier { +//#region Identifiers / Payload + +export interface IBaseWorkspaceIdentifier { + + /** + * Every workspace (multi-root, single folder or empty) + * has a unique identifier. It is not possible to open + * a workspace with the same `id` in multiple windows + */ id: string; +} + +/** + * A single folder workspace identifier is a path to a folder + id. + */ +export interface ISingleFolderWorkspaceIdentifier extends IBaseWorkspaceIdentifier { + + /** + * Folder path as `URI`. + */ + uri: URI; +} + +export function isSingleFolderWorkspaceIdentifier(obj: unknown): obj is ISingleFolderWorkspaceIdentifier { + const singleFolderIdentifier = obj as ISingleFolderWorkspaceIdentifier | undefined; + + return typeof singleFolderIdentifier?.id === 'string' && URI.isUri(singleFolderIdentifier.uri); +} + +/** + * A multi-root workspace identifier is a path to a workspace file + id. + */ +export interface IWorkspaceIdentifier extends IBaseWorkspaceIdentifier { + + /** + * Workspace config file path as `URI`. + */ configPath: URI; } -export function reviveWorkspaceIdentifier(workspace: { id: string, configPath: UriComponents; }): IWorkspaceIdentifier { - return { id: workspace.id, configPath: URI.revive(workspace.configPath) }; +export function toWorkspaceIdentifier(workspace: IWorkspace): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { + + // Multi root + if (workspace.configuration) { + return { + id: workspace.id, + configPath: workspace.configuration + }; + } + + // Single folder + if (workspace.folders.length === 1) { + return { + id: workspace.id, + uri: workspace.folders[0].uri + }; + } + + // Empty workspace + return undefined; } -export function isStoredWorkspaceFolder(thing: unknown): thing is IStoredWorkspaceFolder { - return isRawFileWorkspaceFolder(thing) || isRawUriWorkspaceFolder(thing); +export function isWorkspaceIdentifier(obj: unknown): obj is IWorkspaceIdentifier { + const workspaceIdentifier = obj as IWorkspaceIdentifier | undefined; + + return typeof workspaceIdentifier?.id === 'string' && URI.isUri(workspaceIdentifier.configPath); } -export function isRawFileWorkspaceFolder(thing: any): thing is IRawFileWorkspaceFolder { - return thing - && typeof thing === 'object' - && typeof thing.path === 'string' - && (!thing.name || typeof thing.name === 'string'); +export function reviveIdentifier(identifier: { id: string, uri?: UriComponents, configPath?: UriComponents } | undefined): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { + if (identifier?.uri) { + return { id: identifier.id, uri: URI.revive(identifier.uri) }; + } + + if (identifier?.configPath) { + return { id: identifier.id, configPath: URI.revive(identifier.configPath) }; + } + + return undefined; } -export function isRawUriWorkspaceFolder(thing: any): thing is IRawUriWorkspaceFolder { - return thing - && typeof thing === 'object' - && typeof thing.uri === 'string' - && (!thing.name || typeof thing.name === 'string'); +export function isUntitledWorkspace(path: URI, environmentService: IEnvironmentService): boolean { + return extUriBiasedIgnorePathCase.isEqualOrParent(path, environmentService.untitledWorkspacesHome); +} + +export interface IEmptyWorkspaceInitializationPayload extends IBaseWorkspaceIdentifier { } + +export type IWorkspaceInitializationPayload = IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceInitializationPayload; + +//#endregion + +//#region Workspace File Utilities + +export function isStoredWorkspaceFolder(obj: unknown): obj is IStoredWorkspaceFolder { + return isRawFileWorkspaceFolder(obj) || isRawUriWorkspaceFolder(obj); +} + +export function isRawFileWorkspaceFolder(obj: unknown): obj is IRawFileWorkspaceFolder { + const candidate = obj as IRawFileWorkspaceFolder | undefined; + + return typeof candidate?.path === 'string' && (!candidate.name || typeof candidate.name === 'string'); +} + +export function isRawUriWorkspaceFolder(obj: unknown): obj is IRawUriWorkspaceFolder { + const candidate = obj as IRawUriWorkspaceFolder | undefined; + + return typeof candidate?.uri === 'string' && (!candidate.name || typeof candidate.name === 'string'); } export interface IRawFileWorkspaceFolder { @@ -151,56 +238,6 @@ export interface IEnterWorkspaceResult { backupPath?: string; } -export function isSingleFolderWorkspaceIdentifier(obj: unknown): obj is ISingleFolderWorkspaceIdentifier { - return obj instanceof URI; -} - -export function isWorkspaceIdentifier(obj: unknown): obj is IWorkspaceIdentifier { - const workspaceIdentifier = obj as IWorkspaceIdentifier; - - return workspaceIdentifier && typeof workspaceIdentifier.id === 'string' && workspaceIdentifier.configPath instanceof URI; -} - -export function toWorkspaceIdentifier(workspace: IWorkspace): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { - if (workspace.configuration) { - return { - configPath: workspace.configuration, - id: workspace.id - }; - } - - if (workspace.folders.length === 1) { - return workspace.folders[0].uri; - } - - // Empty workspace - return undefined; -} - -export function isUntitledWorkspace(path: URI, environmentService: IEnvironmentService): boolean { - return extUriBiasedIgnorePathCase.isEqualOrParent(path, environmentService.untitledWorkspacesHome); -} - -export type IMultiFolderWorkspaceInitializationPayload = IWorkspaceIdentifier; -export interface ISingleFolderWorkspaceInitializationPayload { id: string; folder: ISingleFolderWorkspaceIdentifier; } -export interface IEmptyWorkspaceInitializationPayload { id: string; } - -export type IWorkspaceInitializationPayload = IMultiFolderWorkspaceInitializationPayload | ISingleFolderWorkspaceInitializationPayload | IEmptyWorkspaceInitializationPayload; - -export function isSingleFolderWorkspaceInitializationPayload(obj: any): obj is ISingleFolderWorkspaceInitializationPayload { - return isSingleFolderWorkspaceIdentifier((obj.folder as ISingleFolderWorkspaceIdentifier)); -} - -const WORKSPACE_SUFFIX = '.' + WORKSPACE_EXTENSION; - -export function hasWorkspaceFileExtension(path: string | URI) { - const ext = (typeof path === 'string') ? extname(path) : resourceExtname(path); - - return ext === WORKSPACE_SUFFIX; -} - -const SLASH = '/'; - /** * Given a folder URI and the workspace config folder, computes the IStoredWorkspaceFolder using * a relative or absolute path or a uri. @@ -212,12 +249,12 @@ const SLASH = '/'; * @param targetConfigFolderURI the folder where the workspace is living in * @param useSlashForPath if set, use forward slashes for file paths on windows */ -export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows): IStoredWorkspaceFolder { +export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows, extUri: IExtUri): IStoredWorkspaceFolder { if (folderURI.scheme !== targetConfigFolderURI.scheme) { return { name: folderName, uri: folderURI.toString(true) }; } - let folderPath = !forceAbsolute ? relativePath(targetConfigFolderURI, folderURI) : undefined; + let folderPath = !forceAbsolute ? extUri.relativePath(targetConfigFolderURI, folderURI) : undefined; if (folderPath !== undefined) { if (folderPath.length === 0) { folderPath = '.'; @@ -241,7 +278,7 @@ export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, } } } else { - if (!isEqualAuthority(folderURI.authority, targetConfigFolderURI.authority)) { + if (!extUri.isEqualAuthority(folderURI.authority, targetConfigFolderURI.authority)) { return { name: folderName, uri: folderURI.toString(true) }; } folderPath = folderURI.path; @@ -251,21 +288,59 @@ export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, return { name: folderName, path: folderPath }; } +export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], workspaceConfigFile: URI, extUri: IExtUri): WorkspaceFolder[] { + let result: WorkspaceFolder[] = []; + let seen: Set = new Set(); + + const relativeTo = extUri.dirname(workspaceConfigFile); + for (let configuredFolder of configuredFolders) { + let uri: URI | null = null; + if (isRawFileWorkspaceFolder(configuredFolder)) { + if (configuredFolder.path) { + uri = extUri.resolvePath(relativeTo, configuredFolder.path); + } + } else if (isRawUriWorkspaceFolder(configuredFolder)) { + try { + uri = URI.parse(configuredFolder.uri); + // this makes sure all workspace folder are absolute + if (uri.path[0] !== '/') { + uri = uri.with({ path: '/' + uri.path }); + } + } catch (e) { + console.warn(e); + // ignore + } + } + if (uri) { + // remove duplicates + let comparisonKey = extUri.getComparisonKey(uri); + if (!seen.has(comparisonKey)) { + seen.add(comparisonKey); + + const name = configuredFolder.name || extUri.basenameOrAuthority(uri); + result.push(new WorkspaceFolder({ uri, name, index: result.length }, configuredFolder)); + } + } + } + + return result; +} + /** * Rewrites the content of a workspace file to be saved at a new location. * Throws an exception if file is not a valid workspace file */ -export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, isFromUntitledWorkspace: boolean, targetConfigPathURI: URI) { +export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, isFromUntitledWorkspace: boolean, targetConfigPathURI: URI, extUri: IExtUri) { let storedWorkspace = doParseStoredWorkspace(configPathURI, rawWorkspaceContents); - const sourceConfigFolder = dirname(configPathURI); - const targetConfigFolder = dirname(targetConfigPathURI); + const sourceConfigFolder = extUri.dirname(configPathURI); + const targetConfigFolder = extUri.dirname(targetConfigPathURI); const rewrittenFolders: IStoredWorkspaceFolder[] = []; const slashForPath = useSlashForPath(storedWorkspace.folders); for (const folder of storedWorkspace.folders) { - const folderURI = isRawFileWorkspaceFolder(folder) ? resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri); + const folderURI = isRawFileWorkspaceFolder(folder) ? extUri.resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri); let absolute; if (isFromUntitledWorkspace) { // if it was an untitled workspace, try to make paths relative @@ -274,7 +349,7 @@ export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, // for existing workspaces, preserve whether a path was absolute or relative absolute = !isRawFileWorkspaceFolder(folder) || isAbsolute(folder.path); } - rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath)); + rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath, extUri)); } // Preserve as much of the existing workspace as possible by using jsonEdit @@ -308,12 +383,14 @@ function doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { export function useSlashForPath(storedFolders: IStoredWorkspaceFolder[]): boolean { if (isWindows) { - return storedFolders.some(folder => isRawFileWorkspaceFolder(folder) && folder.path.indexOf(SLASH) >= 0); + return storedFolders.some(folder => isRawFileWorkspaceFolder(folder) && folder.path.indexOf('/') >= 0); } return true; } +//#endregion + //#region Workspace Storage interface ISerializedRecentlyOpened { diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index 7a5baa225..adef3f004 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import * as arrays from 'vs/base/common/arrays'; +import { localize } from 'vs/nls'; +import { coalesce } from 'vs/base/common/arrays'; import { IStateService } from 'vs/platform/state/node/state'; -import { app, JumpListCategory } from 'electron'; +import { app, JumpListCategory, JumpListItem } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; import { getBaseLabel, getPathLabel, splitName } from 'vs/base/common/labels'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile, toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData, WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; -import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { IWorkspaceIdentifier, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile, toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData, WORKSPACE_EXTENSION, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { ThrottledDelayer } from 'vs/base/common/async'; -import { isEqual, dirname, originalFSPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { dirname, originalFSPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; @@ -57,15 +57,15 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa declare readonly _serviceBrand: undefined; - private readonly _onRecentlyOpenedChange = new Emitter(); + private readonly _onRecentlyOpenedChange = this._register(new Emitter()); readonly onRecentlyOpenedChange: CommonEvent = this._onRecentlyOpenedChange.event; - private macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer(800)); + private readonly macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer(800)); constructor( @IStateService private readonly stateService: IStateService, @ILogService private readonly logService: ILogService, - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService ) { @@ -80,7 +80,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.handleWindowsJumpList()); // Add to history when entering workspace - this._register(this.workspacesMainService.onWorkspaceEntered(event => this.addRecentlyOpened([{ workspace: event.workspace }]))); + this._register(this.workspacesManagementMainService.onWorkspaceEntered(event => this.addRecentlyOpened([{ workspace: event.workspace }]))); } private handleWindowsJumpList(): void { @@ -89,40 +89,40 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } this.updateWindowsJumpList(); - this.onRecentlyOpenedChange(() => this.updateWindowsJumpList()); + this._register(this.onRecentlyOpenedChange(() => this.updateWindowsJumpList())); } - addRecentlyOpened(newlyAdded: IRecent[]): void { + addRecentlyOpened(recentToAdd: IRecent[]): void { const workspaces: Array = []; const files: IRecentFile[] = []; - for (let curr of newlyAdded) { + for (let recent of recentToAdd) { // Workspace - if (isRecentWorkspace(curr)) { - if (!this.workspacesMainService.isUntitledWorkspace(curr.workspace) && indexOfWorkspace(workspaces, curr.workspace) === -1) { - workspaces.push(curr); + if (isRecentWorkspace(recent)) { + if (!this.workspacesManagementMainService.isUntitledWorkspace(recent.workspace) && indexOfWorkspace(workspaces, recent.workspace) === -1) { + workspaces.push(recent); } } // Folder - else if (isRecentFolder(curr)) { - if (indexOfFolder(workspaces, curr.folderUri) === -1) { - workspaces.push(curr); + else if (isRecentFolder(recent)) { + if (indexOfFolder(workspaces, recent.folderUri) === -1) { + workspaces.push(recent); } } // File else { - const alreadyExistsInHistory = indexOfFile(files, curr.fileUri) >= 0; - const shouldBeFiltered = curr.fileUri.scheme === Schemas.file && WorkspacesHistoryMainService.COMMON_FILES_FILTER.indexOf(basename(curr.fileUri)) >= 0; + const alreadyExistsInHistory = indexOfFile(files, recent.fileUri) >= 0; + const shouldBeFiltered = recent.fileUri.scheme === Schemas.file && WorkspacesHistoryMainService.COMMON_FILES_FILTER.indexOf(basename(recent.fileUri)) >= 0; if (!alreadyExistsInHistory && !shouldBeFiltered) { - files.push(curr); + files.push(recent); // Add to recent documents (Windows only, macOS later) - if (isWindows && curr.fileUri.scheme === Schemas.file) { - app.addRecentDocument(curr.fileUri.fsPath); + if (isWindows && recent.fileUri.scheme === Schemas.file) { + app.addRecentDocument(recent.fileUri.fsPath); } } } @@ -147,14 +147,15 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } } - removeRecentlyOpened(toRemove: URI[]): void { + removeRecentlyOpened(recentToRemove: URI[]): void { const keep = (recent: IRecent) => { const uri = location(recent); - for (const resource of toRemove) { - if (isEqual(resource, uri)) { + for (const resourceToRemove of recentToRemove) { + if (extUriBiasedIgnorePathCase.isEqual(resourceToRemove, uri)) { return false; } } + return true; }; @@ -246,13 +247,10 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Add current workspace to beginning if set const currentWorkspace = include?.config?.workspace; - if (currentWorkspace && !this.workspacesMainService.isUntitledWorkspace(currentWorkspace)) { + if (isWorkspaceIdentifier(currentWorkspace) && !this.workspacesManagementMainService.isUntitledWorkspace(currentWorkspace)) { workspaces.push({ workspace: currentWorkspace }); - } - - const currentFolder = include?.config?.folderUri; - if (currentFolder) { - workspaces.push({ folderUri: currentFolder }); + } else if (isSingleFolderWorkspaceIdentifier(currentWorkspace)) { + workspaces.push({ folderUri: currentWorkspace.uri }); } // Add currently files to open to the beginning if any @@ -319,8 +317,8 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa items: [ { type: 'task', - title: nls.localize('newWindow', "New Window"), - description: nls.localize('newWindowDesc', "Opens a new window"), + title: localize('newWindow', "New Window"), + description: localize('newWindowDesc', "Opens a new window"), program: process.execPath, args: '-n', // force new window iconPath: process.execPath, @@ -330,57 +328,59 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa }); // Recent Workspaces - try { - if (this.getRecentlyOpened().workspaces.length > 0) { + if (this.getRecentlyOpened().workspaces.length > 0) { - // The user might have meanwhile removed items from the jump list and we have to respect that - // so we need to update our list of recent paths with the choice of the user to not add them again - // Also: Windows will not show our custom category at all if there is any entry which was removed - // by the user! See https://github.com/microsoft/vscode/issues/15052 - let toRemove: URI[] = []; - for (let item of app.getJumpListSettings().removedItems) { - const args = item.args; - if (args) { - const match = /^--(folder|file)-uri\s+"([^"]+)"$/.exec(args); - if (match) { - toRemove.push(URI.parse(match[2])); - } + // The user might have meanwhile removed items from the jump list and we have to respect that + // so we need to update our list of recent paths with the choice of the user to not add them again + // Also: Windows will not show our custom category at all if there is any entry which was removed + // by the user! See https://github.com/microsoft/vscode/issues/15052 + let toRemove: URI[] = []; + for (let item of app.getJumpListSettings().removedItems) { + const args = item.args; + if (args) { + const match = /^--(folder|file)-uri\s+"([^"]+)"$/.exec(args); + if (match) { + toRemove.push(URI.parse(match[2])); } } - this.removeRecentlyOpened(toRemove); + } + this.removeRecentlyOpened(toRemove); - // Add entries + // Add entries + let hasWorkspaces = false; + const items: JumpListItem[] = coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(recent => { + const workspace = isRecentWorkspace(recent) ? recent.workspace : recent.folderUri; + const title = recent.label ? splitName(recent.label).name : this.getSimpleWorkspaceLabel(workspace, this.environmentService.untitledWorkspacesHome); + + let description; + let args; + if (URI.isUri(workspace)) { + description = localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(dirname(workspace), this.environmentService)); + args = `--folder-uri "${workspace.toString()}"`; + } else { + hasWorkspaces = true; + description = localize('workspaceDesc', "{0} {1}", getBaseLabel(workspace.configPath), getPathLabel(dirname(workspace.configPath), this.environmentService)); + args = `--file-uri "${workspace.configPath.toString()}"`; + } + + return { + type: 'task', + title: title.substr(0, 255), // Windows seems to be picky around the length of entries + description: description.substr(0, 255), // (see https://github.com/microsoft/vscode/issues/111177) + program: process.execPath, + args, + iconPath: 'explorer.exe', // simulate folder icon + iconIndex: 0 + }; + })); + + if (items.length > 0) { jumpList.push({ type: 'custom', - name: nls.localize('recentFolders', "Recent Workspaces"), - items: arrays.coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(recent => { - const workspace = isRecentWorkspace(recent) ? recent.workspace : recent.folderUri; - const title = recent.label ? splitName(recent.label).name : this.getSimpleWorkspaceLabel(workspace, this.environmentService.untitledWorkspacesHome); - - let description; - let args; - if (isSingleFolderWorkspaceIdentifier(workspace)) { - description = nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(dirname(workspace), this.environmentService)); - args = `--folder-uri "${workspace.toString()}"`; - } else { - description = nls.localize('workspaceDesc', "{0} {1}", getBaseLabel(workspace.configPath), getPathLabel(dirname(workspace.configPath), this.environmentService)); - args = `--file-uri "${workspace.configPath.toString()}"`; - } - - return { - type: 'task', - title, - description, - program: process.execPath, - args, - iconPath: 'explorer.exe', // simulate folder icon - iconIndex: 0 - }; - })) + name: hasWorkspaces ? localize('recentFoldersAndWorkspaces', "Recent Folders & Workspaces") : localize('recentFolders', "Recent Folders"), + items }); } - } catch (error) { - this.logService.warn('updateWindowsJumpList#recentWorkspaces', error); // https://github.com/microsoft/vscode/issues/111177 } // Recent @@ -396,21 +396,24 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } private getSimpleWorkspaceLabel(workspace: IWorkspaceIdentifier | URI, workspaceHome: URI): string { - if (isSingleFolderWorkspaceIdentifier(workspace)) { + + // Single Folder + if (URI.isUri(workspace)) { return basename(workspace); } // Workspace: Untitled if (extUriBiasedIgnorePathCase.isEqualOrParent(workspace.configPath, workspaceHome)) { - return nls.localize('untitledWorkspace', "Untitled (Workspace)"); + return localize('untitledWorkspace', "Untitled (Workspace)"); } + // Workspace: normal let filename = basename(workspace.configPath); if (filename.endsWith(WORKSPACE_EXTENSION)) { filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); } - return nls.localize('workspaceName', "{0} (Workspace)", filename); + return localize('workspaceName', "{0} (Workspace)", filename); } } @@ -430,10 +433,10 @@ function indexOfWorkspace(arr: IRecent[], candidate: IWorkspaceIdentifier): numb return arr.findIndex(workspace => isRecentWorkspace(workspace) && workspace.workspace.id === candidate.id); } -function indexOfFolder(arr: IRecent[], candidate: ISingleFolderWorkspaceIdentifier): number { - return arr.findIndex(folder => isRecentFolder(folder) && isEqual(folder.folderUri, candidate)); +function indexOfFolder(arr: IRecent[], candidate: URI): number { + return arr.findIndex(folder => isRecentFolder(folder) && extUriBiasedIgnorePathCase.isEqual(folder.folderUri, candidate)); } function indexOfFile(arr: IRecentFile[], candidate: URI): number { - return arr.findIndex(file => isEqual(file.fileUri, candidate)); + return arr.findIndex(file => extUriBiasedIgnorePathCase.isEqual(file.fileUri, candidate)); } diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index f4fa065fb..5328a07dd 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -3,337 +3,79 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder, IEnterWorkspaceResult, isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces'; -import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { join, dirname } from 'vs/base/common/path'; -import { mkdirp, writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs'; -import { readFileSync, existsSync, mkdirSync } from 'fs'; -import { isLinux } from 'vs/base/common/platform'; -import { Event, Emitter } from 'vs/base/common/event'; -import { ILogService } from 'vs/platform/log/common/log'; -import { createHash } from 'crypto'; -import * as json from 'vs/base/common/json'; -import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; +import { AddFirstParameterToFunctions } from 'vs/base/common/types'; +import { IWorkspacesService, IEnterWorkspaceResult, IWorkspaceFolderCreationData, IWorkspaceIdentifier, IRecentlyOpened, IRecent } from 'vs/platform/workspaces/common/workspaces'; import { URI } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { originalFSPath, joinPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; -import { localize } from 'vs/nls'; -import product from 'vs/platform/product/common/product'; -import { MessageBoxOptions, BrowserWindow } from 'electron'; -import { withNullAsUndefined } from 'vs/base/common/types'; +import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; +import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; -import { findWindowOnWorkspace } from 'vs/platform/windows/node/window'; -export const IWorkspacesMainService = createDecorator('workspacesMainService'); - -export interface IWorkspaceEnteredEvent { - window: ICodeWindow; - workspace: IWorkspaceIdentifier; -} - -export interface IWorkspacesMainService { - - readonly _serviceBrand: undefined; - - readonly onUntitledWorkspaceDeleted: Event; - readonly onWorkspaceEntered: Event; - - enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise; - - createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise; - createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; - - deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise; - deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void; - - getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[]; - isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; - - resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null; - getWorkspaceIdentifier(workspacePath: URI): Promise; -} - -export interface IStoredWorkspace { - folders: IStoredWorkspaceFolder[]; - remoteAuthority?: string; -} - -export class WorkspacesMainService extends Disposable implements IWorkspacesMainService { +export class WorkspacesMainService implements AddFirstParameterToFunctions /* only methods, not events */, number /* window ID */> { declare readonly _serviceBrand: undefined; - private readonly untitledWorkspacesHome: URI; // local URI that contains all untitled workspaces - - private readonly _onUntitledWorkspaceDeleted = this._register(new Emitter()); - readonly onUntitledWorkspaceDeleted: Event = this._onUntitledWorkspaceDeleted.event; - - private readonly _onWorkspaceEntered = this._register(new Emitter()); - readonly onWorkspaceEntered: Event = this._onWorkspaceEntered.event; - constructor( - @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, - @ILogService private readonly logService: ILogService, - @IBackupMainService private readonly backupMainService: IBackupMainService, - @IDialogMainService private readonly dialogMainService: IDialogMainService + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + @IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService, + @IBackupMainService private readonly backupMainService: IBackupMainService ) { - super(); - - this.untitledWorkspacesHome = environmentService.untitledWorkspacesHome; } - resolveLocalWorkspaceSync(uri: URI): IResolvedWorkspace | null { - if (!this.isWorkspacePath(uri)) { - return null; // does not look like a valid workspace config file - } - if (uri.scheme !== Schemas.file) { - return null; - } + //#region Workspace Management - let contents: string; - try { - contents = readFileSync(uri.fsPath, 'utf8'); - } catch (error) { - return null; // invalid workspace - } - - return this.doResolveWorkspace(uri, contents); - } - - private isWorkspacePath(uri: URI): boolean { - return isUntitledWorkspace(uri, this.environmentService) || hasWorkspaceFileExtension(uri); - } - - private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | null { - try { - const workspace = this.doParseStoredWorkspace(path, contents); - const workspaceIdentifier = getWorkspaceIdentifier(path); - return { - id: workspaceIdentifier.id, - configPath: workspaceIdentifier.configPath, - folders: toWorkspaceFolders(workspace.folders, workspaceIdentifier.configPath), - remoteAuthority: workspace.remoteAuthority - }; - } catch (error) { - this.logService.warn(error.toString()); + async enterWorkspace(windowId: number, path: URI): Promise { + const window = this.windowsMainService.getWindowById(windowId); + if (window) { + return this.workspacesManagementMainService.enterWorkspace(window, this.windowsMainService.getWindows(), path); } return null; } - private doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { - - // Parse workspace file - let storedWorkspace: IStoredWorkspace = json.parse(contents); // use fault tolerant parser - - // Filter out folders which do not have a path or uri set - if (storedWorkspace && Array.isArray(storedWorkspace.folders)) { - storedWorkspace.folders = storedWorkspace.folders.filter(folder => isStoredWorkspaceFolder(folder)); - } else { - throw new Error(`${path.toString(true)} looks like an invalid workspace file.`); - } - - return storedWorkspace; + createUntitledWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { + return this.workspacesManagementMainService.createUntitledWorkspace(folders, remoteAuthority); } - async createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { - const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); - const configPath = workspace.configPath.fsPath; - - await mkdirp(dirname(configPath)); - await writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); - - return workspace; + deleteUntitledWorkspace(windowId: number, workspace: IWorkspaceIdentifier): Promise { + return this.workspacesManagementMainService.deleteUntitledWorkspace(workspace); } - createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): IWorkspaceIdentifier { - const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); - const configPath = workspace.configPath.fsPath; - - const configPathDir = dirname(configPath); - if (!existsSync(configPathDir)) { - const configPathDirDir = dirname(configPathDir); - if (!existsSync(configPathDirDir)) { - mkdirSync(configPathDirDir); - } - mkdirSync(configPathDir); - } - - writeFileSync(configPath, JSON.stringify(storedWorkspace, null, '\t')); - - return workspace; + getWorkspaceIdentifier(windowId: number, workspacePath: URI): Promise { + return this.workspacesManagementMainService.getWorkspaceIdentifier(workspacePath); } - private newUntitledWorkspace(folders: IWorkspaceFolderCreationData[] = [], remoteAuthority?: string): { workspace: IWorkspaceIdentifier, storedWorkspace: IStoredWorkspace } { - const randomId = (Date.now() + Math.round(Math.random() * 1000)).toString(); - const untitledWorkspaceConfigFolder = joinPath(this.untitledWorkspacesHome, randomId); - const untitledWorkspaceConfigPath = joinPath(untitledWorkspaceConfigFolder, UNTITLED_WORKSPACE_NAME); + //#endregion - const storedWorkspaceFolder: IStoredWorkspaceFolder[] = []; + //#region Workspaces History - for (const folder of folders) { - storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, true, folder.name, untitledWorkspaceConfigFolder)); - } + readonly onRecentlyOpenedChange = this.workspacesHistoryMainService.onRecentlyOpenedChange; - return { - workspace: getWorkspaceIdentifier(untitledWorkspaceConfigPath), - storedWorkspace: { folders: storedWorkspaceFolder, remoteAuthority } - }; + async getRecentlyOpened(windowId: number): Promise { + return this.workspacesHistoryMainService.getRecentlyOpened(this.windowsMainService.getWindowById(windowId)); } - async getWorkspaceIdentifier(configPath: URI): Promise { - return getWorkspaceIdentifier(configPath); + async addRecentlyOpened(windowId: number, recents: IRecent[]): Promise { + return this.workspacesHistoryMainService.addRecentlyOpened(recents); } - isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean { - return isUntitledWorkspace(workspace.configPath, this.environmentService); + async removeRecentlyOpened(windowId: number, paths: URI[]): Promise { + return this.workspacesHistoryMainService.removeRecentlyOpened(paths); } - deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { - if (!this.isUntitledWorkspace(workspace)) { - return; // only supported for untitled workspaces - } - - // Delete from disk - this.doDeleteUntitledWorkspaceSync(workspace); - - // Event - this._onUntitledWorkspaceDeleted.fire(workspace); + async clearRecentlyOpened(windowId: number): Promise { + return this.workspacesHistoryMainService.clearRecentlyOpened(); } - async deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise { - this.deleteUntitledWorkspaceSync(workspace); + //#endregion + + + //#region Dirty Workspaces + + async getDirtyWorkspaces(): Promise> { + return this.backupMainService.getDirtyWorkspaces(); } - private doDeleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { - const configPath = originalFSPath(workspace.configPath); - try { - - // Delete Workspace - rimrafSync(dirname(configPath)); - - // Mark Workspace Storage to be deleted - const workspaceStoragePath = join(this.environmentService.workspaceStorageHome.fsPath, workspace.id); - if (existsSync(workspaceStoragePath)) { - writeFileSync(join(workspaceStoragePath, 'obsolete'), ''); - } - } catch (error) { - this.logService.warn(`Unable to delete untitled workspace ${configPath} (${error}).`); - } - } - - getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[] { - let untitledWorkspaces: IUntitledWorkspaceInfo[] = []; - try { - const untitledWorkspacePaths = readdirSync(this.untitledWorkspacesHome.fsPath).map(folder => joinPath(this.untitledWorkspacesHome, folder, UNTITLED_WORKSPACE_NAME)); - for (const untitledWorkspacePath of untitledWorkspacePaths) { - const workspace = getWorkspaceIdentifier(untitledWorkspacePath); - const resolvedWorkspace = this.resolveLocalWorkspaceSync(untitledWorkspacePath); - if (!resolvedWorkspace) { - this.doDeleteUntitledWorkspaceSync(workspace); - } else { - untitledWorkspaces.push({ workspace, remoteAuthority: resolvedWorkspace.remoteAuthority }); - } - } - } catch (error) { - if (error.code !== 'ENOENT') { - this.logService.warn(`Unable to read folders in ${this.untitledWorkspacesHome} (${error}).`); - } - } - return untitledWorkspaces; - } - - async enterWorkspace(window: ICodeWindow, windows: ICodeWindow[], path: URI): Promise { - if (!window || !window.win || !window.isReady) { - return null; // return early if the window is not ready or disposed - } - - const isValid = await this.isValidTargetWorkspacePath(window, windows, path); - if (!isValid) { - return null; // return early if the workspace is not valid - } - - const result = this.doEnterWorkspace(window, getWorkspaceIdentifier(path)); - if (!result) { - return null; - } - - // Emit as event - this._onWorkspaceEntered.fire({ window, workspace: result.workspace }); - - return result; - } - - private async isValidTargetWorkspacePath(window: ICodeWindow, windows: ICodeWindow[], path?: URI): Promise { - if (!path) { - return true; - } - - if (window.openedWorkspace && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, path)) { - return false; // window is already opened on a workspace with that path - } - - // Prevent overwriting a workspace that is currently opened in another window - if (findWindowOnWorkspace(windows, getWorkspaceIdentifier(path))) { - const options: MessageBoxOptions = { - title: product.nameLong, - type: 'info', - buttons: [localize('ok', "OK")], - message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)), - detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), - noLink: true - }; - - await this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); - - return false; - } - - return true; // OK - } - - private doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult | null { - if (!window.config) { - return null; - } - - window.focus(); - - // Register window for backups and migrate current backups over - let backupPath: string | undefined; - if (!window.config.extensionDevelopmentPath) { - backupPath = this.backupMainService.registerWorkspaceBackupSync({ workspace, remoteAuthority: window.remoteAuthority }, window.config.backupPath); - } - - // if the window was opened on an untitled workspace, delete it. - if (window.openedWorkspace && this.isUntitledWorkspace(window.openedWorkspace)) { - this.deleteUntitledWorkspaceSync(window.openedWorkspace); - } - - // Update window configuration properly based on transition to workspace - window.config.folderUri = undefined; - window.config.workspace = workspace; - window.config.backupPath = backupPath; - - return { workspace, backupPath }; - } -} - -function getWorkspaceId(configPath: URI): string { - let workspaceConfigPath = configPath.scheme === Schemas.file ? originalFSPath(configPath) : configPath.toString(); - if (!isLinux) { - workspaceConfigPath = workspaceConfigPath.toLowerCase(); // sanitize for platform file system - } - - return createHash('md5').update(workspaceConfigPath).digest('hex'); -} - -export function getWorkspaceIdentifier(configPath: URI): IWorkspaceIdentifier { - return { - configPath, - id: getWorkspaceId(configPath) - }; + //#endregion } diff --git a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts new file mode 100644 index 000000000..306d1daa5 --- /dev/null +++ b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts @@ -0,0 +1,394 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { toWorkspaceFolders, IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder, IEnterWorkspaceResult, isUntitledWorkspace, isWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { join, dirname } from 'vs/base/common/path'; +import { mkdirp, writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs'; +import { readFileSync, existsSync, mkdirSync, statSync, Stats } from 'fs'; +import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { Event, Emitter } from 'vs/base/common/event'; +import { ILogService } from 'vs/platform/log/common/log'; +import { createHash } from 'crypto'; +import { parse } from 'vs/base/common/json'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { originalFSPath, joinPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { localize } from 'vs/nls'; +import product from 'vs/platform/product/common/product'; +import { MessageBoxOptions, BrowserWindow } from 'electron'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; +import { findWindowOnWorkspaceOrFolder } from 'vs/platform/windows/electron-main/windowsFinder'; + +export const IWorkspacesManagementMainService = createDecorator('workspacesManagementMainService'); + +export interface IWorkspaceEnteredEvent { + window: ICodeWindow; + workspace: IWorkspaceIdentifier; +} + +export interface IWorkspacesManagementMainService { + + readonly _serviceBrand: undefined; + + readonly onUntitledWorkspaceDeleted: Event; + readonly onWorkspaceEntered: Event; + + enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise; + + createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise; + createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; + + deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise; + deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void; + + getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[]; + isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; + + resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null; + getWorkspaceIdentifier(workspacePath: URI): Promise; +} + +export interface IStoredWorkspace { + folders: IStoredWorkspaceFolder[]; + remoteAuthority?: string; +} + +export class WorkspacesManagementMainService extends Disposable implements IWorkspacesManagementMainService { + + declare readonly _serviceBrand: undefined; + + private readonly untitledWorkspacesHome = this.environmentService.untitledWorkspacesHome; // local URI that contains all untitled workspaces + + private readonly _onUntitledWorkspaceDeleted = this._register(new Emitter()); + readonly onUntitledWorkspaceDeleted: Event = this._onUntitledWorkspaceDeleted.event; + + private readonly _onWorkspaceEntered = this._register(new Emitter()); + readonly onWorkspaceEntered: Event = this._onWorkspaceEntered.event; + + constructor( + @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, + @ILogService private readonly logService: ILogService, + @IBackupMainService private readonly backupMainService: IBackupMainService, + @IDialogMainService private readonly dialogMainService: IDialogMainService + ) { + super(); + } + + resolveLocalWorkspaceSync(uri: URI): IResolvedWorkspace | null { + if (!this.isWorkspacePath(uri)) { + return null; // does not look like a valid workspace config file + } + if (uri.scheme !== Schemas.file) { + return null; + } + + let contents: string; + try { + contents = readFileSync(uri.fsPath, 'utf8'); + } catch (error) { + return null; // invalid workspace + } + + return this.doResolveWorkspace(uri, contents); + } + + private isWorkspacePath(uri: URI): boolean { + return isUntitledWorkspace(uri, this.environmentService) || hasWorkspaceFileExtension(uri); + } + + private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | null { + try { + const workspace = this.doParseStoredWorkspace(path, contents); + const workspaceIdentifier = getWorkspaceIdentifier(path); + return { + id: workspaceIdentifier.id, + configPath: workspaceIdentifier.configPath, + folders: toWorkspaceFolders(workspace.folders, workspaceIdentifier.configPath, extUriBiasedIgnorePathCase), + remoteAuthority: workspace.remoteAuthority + }; + } catch (error) { + this.logService.warn(error.toString()); + } + + return null; + } + + private doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { + + // Parse workspace file + const storedWorkspace: IStoredWorkspace = parse(contents); // use fault tolerant parser + + // Filter out folders which do not have a path or uri set + if (storedWorkspace && Array.isArray(storedWorkspace.folders)) { + storedWorkspace.folders = storedWorkspace.folders.filter(folder => isStoredWorkspaceFolder(folder)); + } else { + throw new Error(`${path.toString(true)} looks like an invalid workspace file.`); + } + + return storedWorkspace; + } + + async createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { + const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); + const configPath = workspace.configPath.fsPath; + + await mkdirp(dirname(configPath)); + await writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); + + return workspace; + } + + createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): IWorkspaceIdentifier { + const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); + const configPath = workspace.configPath.fsPath; + + const configPathDir = dirname(configPath); + if (!existsSync(configPathDir)) { + const configPathDirDir = dirname(configPathDir); + if (!existsSync(configPathDirDir)) { + mkdirSync(configPathDirDir); + } + mkdirSync(configPathDir); + } + + writeFileSync(configPath, JSON.stringify(storedWorkspace, null, '\t')); + + return workspace; + } + + private newUntitledWorkspace(folders: IWorkspaceFolderCreationData[] = [], remoteAuthority?: string): { workspace: IWorkspaceIdentifier, storedWorkspace: IStoredWorkspace } { + const randomId = (Date.now() + Math.round(Math.random() * 1000)).toString(); + const untitledWorkspaceConfigFolder = joinPath(this.untitledWorkspacesHome, randomId); + const untitledWorkspaceConfigPath = joinPath(untitledWorkspaceConfigFolder, UNTITLED_WORKSPACE_NAME); + + const storedWorkspaceFolder: IStoredWorkspaceFolder[] = []; + + for (const folder of folders) { + storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, true, folder.name, untitledWorkspaceConfigFolder, !isWindows, extUriBiasedIgnorePathCase)); + } + + return { + workspace: getWorkspaceIdentifier(untitledWorkspaceConfigPath), + storedWorkspace: { folders: storedWorkspaceFolder, remoteAuthority } + }; + } + + async getWorkspaceIdentifier(configPath: URI): Promise { + return getWorkspaceIdentifier(configPath); + } + + isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean { + return isUntitledWorkspace(workspace.configPath, this.environmentService); + } + + deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { + if (!this.isUntitledWorkspace(workspace)) { + return; // only supported for untitled workspaces + } + + // Delete from disk + this.doDeleteUntitledWorkspaceSync(workspace); + + // Event + this._onUntitledWorkspaceDeleted.fire(workspace); + } + + async deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise { + this.deleteUntitledWorkspaceSync(workspace); + } + + private doDeleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { + const configPath = originalFSPath(workspace.configPath); + try { + + // Delete Workspace + rimrafSync(dirname(configPath)); + + // Mark Workspace Storage to be deleted + const workspaceStoragePath = join(this.environmentService.workspaceStorageHome.fsPath, workspace.id); + if (existsSync(workspaceStoragePath)) { + writeFileSync(join(workspaceStoragePath, 'obsolete'), ''); + } + } catch (error) { + this.logService.warn(`Unable to delete untitled workspace ${configPath} (${error}).`); + } + } + + getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[] { + const untitledWorkspaces: IUntitledWorkspaceInfo[] = []; + try { + const untitledWorkspacePaths = readdirSync(this.untitledWorkspacesHome.fsPath).map(folder => joinPath(this.untitledWorkspacesHome, folder, UNTITLED_WORKSPACE_NAME)); + for (const untitledWorkspacePath of untitledWorkspacePaths) { + const workspace = getWorkspaceIdentifier(untitledWorkspacePath); + const resolvedWorkspace = this.resolveLocalWorkspaceSync(untitledWorkspacePath); + if (!resolvedWorkspace) { + this.doDeleteUntitledWorkspaceSync(workspace); + } else { + untitledWorkspaces.push({ workspace, remoteAuthority: resolvedWorkspace.remoteAuthority }); + } + } + } catch (error) { + if (error.code !== 'ENOENT') { + this.logService.warn(`Unable to read folders in ${this.untitledWorkspacesHome} (${error}).`); + } + } + + return untitledWorkspaces; + } + + async enterWorkspace(window: ICodeWindow, windows: ICodeWindow[], path: URI): Promise { + if (!window || !window.win || !window.isReady) { + return null; // return early if the window is not ready or disposed + } + + const isValid = await this.isValidTargetWorkspacePath(window, windows, path); + if (!isValid) { + return null; // return early if the workspace is not valid + } + + const result = this.doEnterWorkspace(window, getWorkspaceIdentifier(path)); + if (!result) { + return null; + } + + // Emit as event + this._onWorkspaceEntered.fire({ window, workspace: result.workspace }); + + return result; + } + + private async isValidTargetWorkspacePath(window: ICodeWindow, windows: ICodeWindow[], workspacePath?: URI): Promise { + if (!workspacePath) { + return true; + } + + if (isWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, workspacePath)) { + return false; // window is already opened on a workspace with that path + } + + // Prevent overwriting a workspace that is currently opened in another window + if (findWindowOnWorkspaceOrFolder(windows, workspacePath)) { + const options: MessageBoxOptions = { + title: product.nameLong, + type: 'info', + buttons: [localize('ok', "OK")], + message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(workspacePath)), + detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), + noLink: true + }; + + await this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); + + return false; + } + + return true; // OK + } + + private doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult | null { + if (!window.config) { + return null; + } + + window.focus(); + + // Register window for backups and migrate current backups over + let backupPath: string | undefined; + if (!window.config.extensionDevelopmentPath) { + backupPath = this.backupMainService.registerWorkspaceBackupSync({ workspace, remoteAuthority: window.remoteAuthority }, window.config.backupPath); + } + + // if the window was opened on an untitled workspace, delete it. + if (isWorkspaceIdentifier(window.openedWorkspace) && this.isUntitledWorkspace(window.openedWorkspace)) { + this.deleteUntitledWorkspaceSync(window.openedWorkspace); + } + + // Update window configuration properly based on transition to workspace + window.config.workspace = workspace; + window.config.backupPath = backupPath; + + return { workspace, backupPath }; + } +} + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// NOTE: DO NOT CHANGE. IDENTIFIERS HAVE TO REMAIN STABLE +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +export function getWorkspaceIdentifier(configPath: URI): IWorkspaceIdentifier { + + function getWorkspaceId(): string { + let configPathStr = configPath.scheme === Schemas.file ? originalFSPath(configPath) : configPath.toString(); + if (!isLinux) { + configPathStr = configPathStr.toLowerCase(); // sanitize for platform file system + } + + return createHash('md5').update(configPathStr).digest('hex'); + } + + return { + id: getWorkspaceId(), + configPath + }; +} + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// NOTE: DO NOT CHANGE. IDENTIFIERS HAVE TO REMAIN STABLE +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +export function getSingleFolderWorkspaceIdentifier(folderUri: URI): ISingleFolderWorkspaceIdentifier | undefined; +export function getSingleFolderWorkspaceIdentifier(folderUri: URI, folderStat: Stats): ISingleFolderWorkspaceIdentifier; +export function getSingleFolderWorkspaceIdentifier(folderUri: URI, folderStat?: Stats): ISingleFolderWorkspaceIdentifier | undefined { + + function getFolderId(): string | undefined { + + // Remote: produce a hash from the entire URI + if (folderUri.scheme !== Schemas.file) { + return createHash('md5').update(folderUri.toString()).digest('hex'); + } + + // Local: produce a hash from the path and include creation time as salt + if (!folderStat) { + try { + folderStat = statSync(folderUri.fsPath); + } catch (error) { + return undefined; // folder does not exist + } + } + + let ctime: number | undefined; + if (isLinux) { + ctime = folderStat.ino; // Linux: birthtime is ctime, so we cannot use it! We use the ino instead! + } else if (isMacintosh) { + ctime = folderStat.birthtime.getTime(); // macOS: birthtime is fine to use as is + } else if (isWindows) { + if (typeof folderStat.birthtimeMs === 'number') { + ctime = Math.floor(folderStat.birthtimeMs); // Windows: fix precision issue in node.js 8.x to get 7.x results (see https://github.com/nodejs/node/issues/19897) + } else { + ctime = folderStat.birthtime.getTime(); + } + } + + // we use the ctime as extra salt to the ID so that we catch the case of a folder getting + // deleted and recreated. in that case we do not want to carry over previous state + return createHash('md5').update(folderUri.fsPath).update(ctime ? String(ctime) : '').digest('hex'); + } + + const folderId = getFolderId(); + if (typeof folderId === 'string') { + return { + id: folderId, + uri: folderUri + }; + } + + return undefined; // invalid folder +} diff --git a/src/vs/platform/workspaces/electron-main/workspacesService.ts b/src/vs/platform/workspaces/electron-main/workspacesService.ts deleted file mode 100644 index 8d1d22d9e..000000000 --- a/src/vs/platform/workspaces/electron-main/workspacesService.ts +++ /dev/null @@ -1,81 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AddFirstParameterToFunctions } from 'vs/base/common/types'; -import { IWorkspacesService, IEnterWorkspaceResult, IWorkspaceFolderCreationData, IWorkspaceIdentifier, IRecentlyOpened, IRecent } from 'vs/platform/workspaces/common/workspaces'; -import { URI } from 'vs/base/common/uri'; -import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; -import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; - -export class WorkspacesService implements AddFirstParameterToFunctions /* only methods, not events */, number /* window ID */> { - - declare readonly _serviceBrand: undefined; - - constructor( - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, - @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService, - @IBackupMainService private readonly backupMainService: IBackupMainService - ) { - } - - //#region Workspace Management - - async enterWorkspace(windowId: number, path: URI): Promise { - const window = this.windowsMainService.getWindowById(windowId); - if (window) { - return this.workspacesMainService.enterWorkspace(window, this.windowsMainService.getWindows(), path); - } - - return null; - } - - createUntitledWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { - return this.workspacesMainService.createUntitledWorkspace(folders, remoteAuthority); - } - - deleteUntitledWorkspace(windowId: number, workspace: IWorkspaceIdentifier): Promise { - return this.workspacesMainService.deleteUntitledWorkspace(workspace); - } - - getWorkspaceIdentifier(windowId: number, workspacePath: URI): Promise { - return this.workspacesMainService.getWorkspaceIdentifier(workspacePath); - } - - //#endregion - - //#region Workspaces History - - readonly onRecentlyOpenedChange = this.workspacesHistoryMainService.onRecentlyOpenedChange; - - async getRecentlyOpened(windowId: number): Promise { - return this.workspacesHistoryMainService.getRecentlyOpened(this.windowsMainService.getWindowById(windowId)); - } - - async addRecentlyOpened(windowId: number, recents: IRecent[]): Promise { - return this.workspacesHistoryMainService.addRecentlyOpened(recents); - } - - async removeRecentlyOpened(windowId: number, paths: URI[]): Promise { - return this.workspacesHistoryMainService.removeRecentlyOpened(paths); - } - - async clearRecentlyOpened(windowId: number): Promise { - return this.workspacesHistoryMainService.clearRecentlyOpened(); - } - - //#endregion - - - //#region Dirty Workspaces - - async getDirtyWorkspaces(): Promise> { - return this.backupMainService.getDirtyWorkspaces(); - } - - //#endregion -} diff --git a/src/vs/platform/workspaces/test/common/workspaces.test.ts b/src/vs/platform/workspaces/test/common/workspaces.test.ts new file mode 100644 index 000000000..2c0a75656 --- /dev/null +++ b/src/vs/platform/workspaces/test/common/workspaces.test.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { hasWorkspaceFileExtension, toWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; + +suite('Workspaces', () => { + test('hasWorkspaceFileExtension', () => { + assert.strictEqual(hasWorkspaceFileExtension('something'), false); + assert.strictEqual(hasWorkspaceFileExtension('something.code-workspace'), true); + }); + + test('toWorkspaceIdentifier', () => { + let identifier = toWorkspaceIdentifier({ id: 'id', folders: [] }); + assert.ok(!identifier); + assert.ok(!isSingleFolderWorkspaceIdentifier(identifier)); + assert.ok(!isWorkspaceIdentifier(identifier)); + + identifier = toWorkspaceIdentifier({ id: 'id', folders: [{ index: 0, name: 'test', toResource: () => URI.file('test'), uri: URI.file('test') }] }); + assert.ok(identifier); + assert.ok(isSingleFolderWorkspaceIdentifier(identifier)); + assert.ok(!isWorkspaceIdentifier(identifier)); + + identifier = toWorkspaceIdentifier({ id: 'id', configuration: URI.file('test.code-workspace'), folders: [] }); + assert.ok(identifier); + assert.ok(!isSingleFolderWorkspaceIdentifier(identifier)); + assert.ok(isWorkspaceIdentifier(identifier)); + }); +}); diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts index c16373206..6f5027448 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts @@ -17,25 +17,25 @@ function toWorkspace(uri: URI): IWorkspaceIdentifier { }; } function assertEqualURI(u1: URI | undefined, u2: URI | undefined, message?: string): void { - assert.equal(u1 && u1.toString(), u2 && u2.toString(), message); + assert.strictEqual(u1 && u1.toString(), u2 && u2.toString(), message); } function assertEqualWorkspace(w1: IWorkspaceIdentifier | undefined, w2: IWorkspaceIdentifier | undefined, message?: string): void { if (!w1 || !w2) { - assert.equal(w1, w2, message); + assert.strictEqual(w1, w2, message); return; } - assert.equal(w1.id, w2.id, message); + assert.strictEqual(w1.id, w2.id, message); assertEqualURI(w1.configPath, w2.configPath, message); } function assertEqualRecentlyOpened(actual: IRecentlyOpened, expected: IRecentlyOpened, message?: string) { - assert.equal(actual.files.length, expected.files.length, message); + assert.strictEqual(actual.files.length, expected.files.length, message); for (let i = 0; i < actual.files.length; i++) { assertEqualURI(actual.files[i].fileUri, expected.files[i].fileUri, message); - assert.equal(actual.files[i].label, expected.files[i].label); + assert.strictEqual(actual.files[i].label, expected.files[i].label); } - assert.equal(actual.workspaces.length, expected.workspaces.length, message); + assert.strictEqual(actual.workspaces.length, expected.workspaces.length, message); for (let i = 0; i < actual.workspaces.length; i++) { let expectedRecent = expected.workspaces[i]; let actualRecent = actual.workspaces[i]; @@ -44,7 +44,7 @@ function assertEqualRecentlyOpened(actual: IRecentlyOpened, expected: IRecentlyO } else { assertEqualWorkspace(actualRecent.workspace, (expectedRecent).workspace, message); } - assert.equal(actualRecent.label, expectedRecent.label); + assert.strictEqual(actualRecent.label, expectedRecent.label); } } @@ -129,8 +129,5 @@ suite('History Storage', () => { }; assertEqualRecentlyOpened(windowsState, expected, 'v1_33'); - }); - - }); diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts similarity index 77% rename from src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts rename to src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts index 07304aa32..2cbc34f48 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts @@ -10,15 +10,15 @@ import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; import { EnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; -import { WorkspacesMainService, IStoredWorkspace } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { WorkspacesManagementMainService, IStoredWorkspace, getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { WORKSPACE_EXTENSION, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder, rewriteWorkspaceFileForNewLocation, IWorkspaceIdentifier, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { NullLogService } from 'vs/platform/log/common/log'; import { URI } from 'vs/base/common/uri'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { isWindows } from 'vs/base/common/platform'; import { normalizeDriveLetter } from 'vs/base/common/labels'; -import { dirname, joinPath } from 'vs/base/common/resources'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { dirname, extUriBiasedIgnorePathCase, joinPath } from 'vs/base/common/resources'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup'; import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; @@ -104,15 +104,7 @@ export class TestBackupMainService implements IBackupMainService { } } -suite('WorkspacesMainService', () => { - const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'workspacesservice'); - const untitledWorkspacesHomePath = path.join(parentDir, 'Workspaces'); - - class TestEnvironmentService extends EnvironmentMainService { - get untitledWorkspacesHome(): URI { - return URI.file(untitledWorkspacesHomePath); - } - } +suite('WorkspacesManagementMainService', () => { function createUntitledWorkspace(folders: string[], names?: string[]) { return service.createUntitledWorkspace(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData))); @@ -138,22 +130,31 @@ suite('WorkspacesMainService', () => { return service.createUntitledWorkspaceSync(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData))); } - const environmentService = new TestEnvironmentService(parseArgs(process.argv, OPTIONS)); - const logService = new NullLogService(); - - let service: WorkspacesMainService; + let testDir: string; + let untitledWorkspacesHomePath: string; + let environmentService: EnvironmentMainService; + let service: WorkspacesManagementMainService; setup(async () => { - service = new WorkspacesMainService(environmentService, logService, new TestBackupMainService(), new TestDialogMainService()); + testDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'workspacesmanagementmainservice'); + untitledWorkspacesHomePath = path.join(testDir, 'Workspaces'); - // Delete any existing backups completely and then re-create it. - await pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE); + environmentService = new class TestEnvironmentService extends EnvironmentMainService { + constructor() { + super(parseArgs(process.argv, OPTIONS)); + } + get untitledWorkspacesHome(): URI { + return URI.file(untitledWorkspacesHomePath); + } + }; + + service = new WorkspacesManagementMainService(environmentService, new NullLogService(), new TestBackupMainService(), new TestDialogMainService()); return pfs.mkdirp(untitledWorkspacesHomePath); }); teardown(() => { - return pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE); + return pfs.rimraf(testDir); }); function assertPathEquals(p1: string, p2: string): void { @@ -162,11 +163,11 @@ suite('WorkspacesMainService', () => { p2 = normalizeDriveLetter(p2); } - assert.equal(p1, p2); + assert.strictEqual(p1, p2); } function assertEqualURI(u1: URI, u2: URI): void { - assert.equal(u1.toString(), u2.toString()); + assert.strictEqual(u1.toString(), u2.toString()); } test('createWorkspace (folders)', async () => { @@ -176,7 +177,7 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace); - assert.equal(ws.folders.length, 2); + assert.strictEqual(ws.folders.length, 2); assertPathEquals((ws.folders[0]).path, process.cwd()); assertPathEquals((ws.folders[1]).path, os.tmpdir()); assert.ok(!(ws.folders[0]).name); @@ -190,11 +191,11 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace); - assert.equal(ws.folders.length, 2); + assert.strictEqual(ws.folders.length, 2); assertPathEquals((ws.folders[0]).path, process.cwd()); assertPathEquals((ws.folders[1]).path, os.tmpdir()); - assert.equal((ws.folders[0]).name, 'currentworkingdirectory'); - assert.equal((ws.folders[1]).name, 'tempdir'); + assert.strictEqual((ws.folders[0]).name, 'currentworkingdirectory'); + assert.strictEqual((ws.folders[1]).name, 'tempdir'); }); test('createUntitledWorkspace (folders as other resource URIs)', async () => { @@ -207,12 +208,12 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace); - assert.equal(ws.folders.length, 2); - assert.equal((ws.folders[0]).uri, folder1URI.toString(true)); - assert.equal((ws.folders[1]).uri, folder2URI.toString(true)); + assert.strictEqual(ws.folders.length, 2); + assert.strictEqual((ws.folders[0]).uri, folder1URI.toString(true)); + assert.strictEqual((ws.folders[1]).uri, folder2URI.toString(true)); assert.ok(!(ws.folders[0]).name); assert.ok(!(ws.folders[1]).name); - assert.equal(ws.remoteAuthority, 'server'); + assert.strictEqual(ws.remoteAuthority, 'server'); }); test('createWorkspaceSync (folders)', () => { @@ -222,7 +223,7 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.equal(ws.folders.length, 2); + assert.strictEqual(ws.folders.length, 2); assertPathEquals((ws.folders[0]).path, process.cwd()); assertPathEquals((ws.folders[1]).path, os.tmpdir()); @@ -237,12 +238,12 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.equal(ws.folders.length, 2); + assert.strictEqual(ws.folders.length, 2); assertPathEquals((ws.folders[0]).path, process.cwd()); assertPathEquals((ws.folders[1]).path, os.tmpdir()); - assert.equal((ws.folders[0]).name, 'currentworkingdirectory'); - assert.equal((ws.folders[1]).name, 'tempdir'); + assert.strictEqual((ws.folders[0]).name, 'currentworkingdirectory'); + assert.strictEqual((ws.folders[1]).name, 'tempdir'); }); test('createUntitledWorkspaceSync (folders as other resource URIs)', () => { @@ -255,9 +256,9 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.equal(ws.folders.length, 2); - assert.equal((ws.folders[0]).uri, folder1URI.toString(true)); - assert.equal((ws.folders[1]).uri, folder2URI.toString(true)); + assert.strictEqual(ws.folders.length, 2); + assert.strictEqual((ws.folders[0]).uri, folder1URI.toString(true)); + assert.strictEqual((ws.folders[1]).uri, folder2URI.toString(true)); assert.ok(!(ws.folders[0]).name); assert.ok(!(ws.folders[1]).name); @@ -273,7 +274,7 @@ suite('WorkspacesMainService', () => { workspace.configPath = URI.file(newPath); const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); - assert.equal(2, resolved!.folders.length); + assert.strictEqual(2, resolved!.folders.length); assertEqualURI(resolved!.configPath, workspace.configPath); assert.ok(resolved!.id); fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ something: 'something' })); // invalid workspace @@ -325,39 +326,39 @@ suite('WorkspacesMainService', () => { let origConfigPath = URI.file(firstConfigPath); let workspaceConfigPath = URI.file(path.join(tmpDir, 'inside', 'myworkspace1.code-workspace')); - let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, false, workspaceConfigPath); + let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); let ws = (JSON.parse(newContent) as IStoredWorkspace); - assert.equal(ws.folders.length, 3); + assert.strictEqual(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); // absolute path because outside of tmpdir assertPathEquals((ws.folders[1]).path, '.'); assertPathEquals((ws.folders[2]).path, 'somefolder'); origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.file(path.join(tmpDir, 'myworkspace2.code-workspace')); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); ws = (JSON.parse(newContent) as IStoredWorkspace); - assert.equal(ws.folders.length, 3); + assert.strictEqual(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); assertPathEquals((ws.folders[1]).path, 'inside'); assertPathEquals((ws.folders[2]).path, isWindows ? 'inside\\somefolder' : 'inside/somefolder'); origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.file(path.join(tmpDir, 'other', 'myworkspace2.code-workspace')); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); ws = (JSON.parse(newContent) as IStoredWorkspace); - assert.equal(ws.folders.length, 3); + assert.strictEqual(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); assertPathEquals((ws.folders[1]).path, isWindows ? '..\\inside' : '../inside'); assertPathEquals((ws.folders[2]).path, isWindows ? '..\\inside\\somefolder' : '../inside/somefolder'); origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.parse('foo://foo/bar/myworkspace2.code-workspace'); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); ws = (JSON.parse(newContent) as IStoredWorkspace); - assert.equal(ws.folders.length, 3); - assert.equal((ws.folders[0]).uri, URI.file(folder1).toString(true)); - assert.equal((ws.folders[1]).uri, URI.file(tmpInsideDir).toString(true)); - assert.equal((ws.folders[2]).uri, URI.file(path.join(tmpInsideDir, 'somefolder')).toString(true)); + assert.strictEqual(ws.folders.length, 3); + assert.strictEqual((ws.folders[0]).uri, URI.file(folder1).toString(true)); + assert.strictEqual((ws.folders[1]).uri, URI.file(tmpInsideDir).toString(true)); + assert.strictEqual((ws.folders[2]).uri, URI.file(path.join(tmpInsideDir, 'somefolder')).toString(true)); fs.unlinkSync(firstConfigPath); }); @@ -369,8 +370,8 @@ suite('WorkspacesMainService', () => { let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); origContent = `// this is a comment\n${origContent}`; - let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); - assert.equal(0, newContent.indexOf('// this is a comment')); + let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); + assert.strictEqual(0, newContent.indexOf('// this is a comment')); service.deleteUntitledWorkspaceSync(workspace); }); @@ -381,26 +382,22 @@ suite('WorkspacesMainService', () => { let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); origContent = origContent.replace(/[\\]/g, '/'); // convert backslash to slash - const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); + const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); const ws = (JSON.parse(newContent) as IStoredWorkspace); assert.ok(ws.folders.every(f => (f).path.indexOf('\\') < 0)); service.deleteUntitledWorkspaceSync(workspace); }); - test.skip('rewriteWorkspaceFileForNewLocation (unc paths)', async () => { - if (!isWindows) { - return Promise.resolve(); - } - + (!isWindows ? test.skip : test)('rewriteWorkspaceFileForNewLocation (unc paths)', async () => { const workspaceLocation = path.join(os.tmpdir(), 'wsloc'); const folder1Location = 'x:\\foo'; const folder2Location = '\\\\server\\share2\\some\\path'; - const folder3Location = path.join(os.tmpdir(), 'wsloc', 'inner', 'more'); + const folder3Location = path.join(workspaceLocation, 'inner', 'more'); const workspace = await createUntitledWorkspace([folder1Location, folder2Location, folder3Location]); const workspaceConfigPath = URI.file(path.join(workspaceLocation, `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`)); let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); - const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); + const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, true, workspaceConfigPath, extUriBiasedIgnorePathCase); const ws = (JSON.parse(newContent) as IStoredWorkspace); assertPathEquals((ws.folders[0]).path, folder1Location); assertPathEquals((ws.folders[1]).path, folder2Location); @@ -422,17 +419,15 @@ suite('WorkspacesMainService', () => { }); test('getUntitledWorkspaceSync', async function () { - this.retries(3); - let untitled = service.getUntitledWorkspacesSync(); - assert.equal(untitled.length, 0); + assert.strictEqual(untitled.length, 0); const untitledOne = await createUntitledWorkspace([process.cwd(), os.tmpdir()]); assert.ok(fs.existsSync(untitledOne.configPath.fsPath)); untitled = service.getUntitledWorkspacesSync(); - assert.equal(1, untitled.length); - assert.equal(untitledOne.id, untitled[0].workspace.id); + assert.strictEqual(1, untitled.length); + assert.strictEqual(untitledOne.id, untitled[0].workspace.id); const untitledTwo = await createUntitledWorkspace([os.tmpdir(), process.cwd()]); assert.ok(fs.existsSync(untitledTwo.configPath.fsPath)); @@ -444,14 +439,50 @@ suite('WorkspacesMainService', () => { if (untitled.length === 1) { assert.fail(`Unexpected workspaces count of 1 (expected 2), all workspaces:\n ${fs.readdirSync(untitledHome.fsPath).map(name => fs.readFileSync(joinPath(untitledHome, name, 'workspace.json').fsPath, 'utf8'))}, before getUntitledWorkspacesSync: ${beforeGettingUntitledWorkspaces}`); } - assert.equal(2, untitled.length); + assert.strictEqual(2, untitled.length); service.deleteUntitledWorkspaceSync(untitledOne); untitled = service.getUntitledWorkspacesSync(); - assert.equal(1, untitled.length); + assert.strictEqual(1, untitled.length); service.deleteUntitledWorkspaceSync(untitledTwo); untitled = service.getUntitledWorkspacesSync(); - assert.equal(0, untitled.length); + assert.strictEqual(0, untitled.length); + }); + + test('getSingleWorkspaceIdentifier', async function () { + const nonLocalUri = URI.parse('myscheme://server/work/p/f1'); + const nonLocalUriId = getSingleFolderWorkspaceIdentifier(nonLocalUri); + assert.ok(nonLocalUriId?.id); + + const localNonExistingUri = URI.file(path.join(testDir, 'f1')); + const localNonExistingUriId = getSingleFolderWorkspaceIdentifier(localNonExistingUri); + assert.ok(!localNonExistingUriId); + + fs.mkdirSync(path.join(testDir, 'f1')); + + const localExistingUri = URI.file(path.join(testDir, 'f1')); + const localExistingUriId = getSingleFolderWorkspaceIdentifier(localExistingUri); + assert.ok(localExistingUriId?.id); + }); + + test('workspace identifiers are stable', function () { + + // workspace identifier (local) + assert.strictEqual(getWorkspaceIdentifier(URI.file('/hello/test')).id, isWindows /* slash vs backslash */ ? '9f3efb614e2cd7924e4b8076e6c72233' : 'e36736311be12ff6d695feefe415b3e8'); + + // single folder identifier (local) + const fakeStat = { + ino: 1611312115129, + birthtimeMs: 1611312115129, + birthtime: new Date(1611312115129) + }; + assert.strictEqual(getSingleFolderWorkspaceIdentifier(URI.file('/hello/test'), fakeStat as fs.Stats)?.id, isWindows /* slash vs backslash */ ? '9a8441e897e5174fa388bc7ef8f7a710' : '1d726b3d516dc2a6d343abf4797eaaef'); + + // workspace identifier (remote) + assert.strictEqual(getWorkspaceIdentifier(URI.parse('vscode-remote:/hello/test')).id, '786de4f224d57691f218dc7f31ee2ee3'); + + // single folder identifier (remote) + assert.strictEqual(getSingleFolderWorkspaceIdentifier(URI.parse('vscode-remote:/hello/test'))?.id, '786de4f224d57691f218dc7f31ee2ee3'); }); }); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index e1c2dd30c..7c5c6828b 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1450,6 +1450,21 @@ declare module 'vscode' { dispose(): void; } + /** + * An error type that should be used to signal cancellation of an operation. + * + * This type can be used in response to a [cancellation token](#CancellationToken) + * being cancelled or when an operation is being cancelled by the + * executor of that operation. + */ + export class CancellationError extends Error { + + /** + * Creates a new cancellation error. + */ + constructor(); + } + /** * Represents a type which can release resources, such * as event listening or a timer. @@ -1700,8 +1715,8 @@ declare module 'vscode' { /** * Options to configure the behaviour of a file open dialog. * - * * Note 1: A dialog can select files, folders, or both. This is not true for Windows - * which enforces to open either files or folder, but *not both*. + * * Note 1: On Windows and Linux, a file dialog cannot be both a file selector and a folder selector, so if you + * set both `canSelectFiles` and `canSelectFolders` to `true` on these platforms, a folder selector will be shown. * * Note 2: Explicitly setting `canSelectFiles` and `canSelectFolders` to `false` is futile * and the editor then silently adjusts the options to select files. */ @@ -3870,8 +3885,8 @@ declare module 'vscode' { * * Note that `sortText` is only used for the initial ordering of completion * items. When having a leading word (prefix) ordering is based on how - * well completion match that prefix and the initial ordering is only used - * when completions match equal. The prefix is defined by the + * well completions match that prefix and the initial ordering is only used + * when completions match equally well. The prefix is defined by the * [`range`](#CompletionItem.range)-property and can therefore be different * for each completion. */ @@ -3884,7 +3899,6 @@ declare module 'vscode' { * * Note that the filter text is matched against the leading word (prefix) which is defined * by the [`range`](#CompletionItem.range)-property. - * prefix. */ filterText?: string; @@ -4683,6 +4697,10 @@ declare module 'vscode' { * This rule will only execute if the text after the cursor matches this regular expression. */ afterText?: RegExp; + /** + * This rule will only execute if the text above the current line matches this regular expression. + */ + previousLineText?: RegExp; /** * The action to execute. */ @@ -5173,9 +5191,9 @@ declare module 'vscode' { set(uri: Uri, diagnostics: ReadonlyArray | undefined): void; /** - * Replace all entries in this collection. + * Replace diagnostics for multiple resources in this collection. * - * Diagnostics of multiple tuples of the same uri will be merged, e.g + * _Note_ that multiple tuples of the same uri will be merged, e.g * `[[file1, [d1]], [file1, [d2]]]` is equivalent to `[[file1, [d1, d2]]]`. * If a diagnostics item is `undefined` as in `[file1, undefined]` * all previous but not subsequent diagnostics are removed. @@ -5419,6 +5437,18 @@ declare module 'vscode' { */ color: string | ThemeColor | undefined; + /** + * The background color for this entry. + * + * *Note*: only `new ThemeColor('statusBarItem.errorBackground')` is + * supported for now. More background colors may be supported in the + * future. + * + * *Note*: when a background color is set, the statusbar may override + * the `color` choice to ensure the entry is readable in all themes. + */ + backgroundColor: ThemeColor | undefined; + /** * [`Command`](#Command) or identifier of a command to run on click. * @@ -5795,6 +5825,11 @@ declare module 'vscode' { setKeysForSync(keys: string[]): void; }; + /** + * A storage utility for secrets. + */ + readonly secrets: SecretStorage; + /** * The uri of the directory containing the extension. */ @@ -5932,6 +5967,48 @@ declare module 'vscode' { update(key: string, value: any): Thenable; } + /** + * The event data that is fired when a secret is added or removed. + */ + export interface SecretStorageChangeEvent { + /** + * The key of the secret that has changed. + */ + readonly key: string; + } + + /** + * Represents a storage utility for secrets, information that is + * sensitive. + */ + export interface SecretStorage { + /** + * Retrieve a secret that was stored with key. Returns undefined if there + * is no password matching that key. + * @param key The key the secret was stored under. + * @returns The stored value or `undefined`. + */ + get(key: string): Thenable; + + /** + * Store a secret under a given key. + * @param key The key to store the secret under. + * @param value The secret. + */ + store(key: string, value: string): Thenable; + + /** + * Remove a secret from storage. + * @param key The key the secret was stored under. + */ + delete(key: string): Thenable; + + /** + * Fires when a secret is stored or deleted. + */ + onDidChange: Event; + } + /** * Represents a color theme kind. */ @@ -7732,7 +7809,7 @@ declare module 'vscode' { * your extension should first check to see if any backups exist for the resource. If there is a backup, your * extension should load the file contents from there instead of from the resource in the workspace. * - * `backup` is triggered approximately one second after the the user stops editing the document. If the user + * `backup` is triggered approximately one second after the user stops editing the document. If the user * rapidly edits the document, `backup` will not be invoked until the editing stops. * * `backup` is not invoked when `auto save` is enabled (since auto save already persists the resource). @@ -8855,7 +8932,8 @@ declare module 'vscode' { getParent?(element: T): ProviderResult; /** - * Called only on hover to resolve the [TreeItem](#TreeItem.tooltip) property if it is undefined. + * Called on hover to resolve the [TreeItem](#TreeItem.tooltip) property if it is undefined. + * Called on tree item click/open to resolve the [TreeItem](#TreeItem.command) property if it is undefined. * Only properties that were undefined can be resolved in `resolveTreeItem`. * Functionality may be expanded later to include being called to resolve other missing * properties on selection and/or on open. @@ -8865,15 +8943,16 @@ declare module 'vscode' { * onDidChangeTreeData should not be triggered from within resolveTreeItem. * * *Note* that this function is called when tree items are already showing in the UI. - * Because of that, no property that changes the presentation (label, description, command, etc.) + * Because of that, no property that changes the presentation (label, description, etc.) * can be changed. * - * @param element The object associated with the TreeItem * @param item Undefined properties of `item` should be set then `item` should be returned. + * @param element The object associated with the TreeItem. + * @param token A cancellation token. * @return The resolved tree item or a thenable that resolves to such. It is OK to return the given * `item`. When no result is returned, the given `item` will be used. */ - resolveTreeItem?(item: TreeItem, element: T): ProviderResult; + resolveTreeItem?(item: TreeItem, element: T, token: CancellationToken): ProviderResult; } export class TreeItem { @@ -9194,7 +9273,7 @@ declare module 'vscode' { * Implement to handle when the number of rows and columns that fit into the terminal panel * changes, for example when font size changes or when the panel is resized. The initial * state of a terminal's dimensions should be treated as `undefined` until this is triggered - * as the size of a terminal isn't know until it shows up in the user interface. + * as the size of a terminal isn't known until it shows up in the user interface. * * When dimensions are overridden by * [onDidOverrideDimensions](#Pseudoterminal.onDidOverrideDimensions), `setDimensions` will @@ -11865,8 +11944,6 @@ declare module 'vscode' { export const onDidChange: Event; } - //#region Comments - /** * Collapsible state of a [comment thread](#CommentThread) */ @@ -12133,7 +12210,7 @@ declare module 'vscode' { /** * Optional reaction handler for creating and deleting reactions on a [comment](#Comment). */ - reactionHandler?: (comment: Comment, reaction: CommentReaction) => Promise; + reactionHandler?: (comment: Comment, reaction: CommentReaction) => Thenable; /** * Dispose this comment controller. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 19e8d8f10..0aa96cf73 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -16,7 +16,7 @@ declare module 'vscode' { - // #region auth provider: https://github.com/microsoft/vscode/issues/88309 + //#region auth provider: https://github.com/microsoft/vscode/issues/88309 /** * An [event](#Event) which fires when an [AuthenticationProvider](#AuthenticationProvider) is added or removed. @@ -54,28 +54,9 @@ declare module 'vscode' { } /** - * **WARNING** When writing an AuthenticationProvider, `id` should be treated as part of your extension's - * API, changing it is a breaking change for all extensions relying on the provider. The id is - * treated case-sensitively. + * A provider for performing authentication to a service. */ export interface AuthenticationProvider { - /** - * Used as an identifier for extensions trying to work with a particular - * provider: 'microsoft', 'github', etc. id must be unique, registering - * another provider with the same id will fail. - */ - readonly id: string; - - /** - * The human-readable name of the provider. - */ - readonly label: string; - - /** - * Whether it is possible to be signed into multiple accounts at once with this provider - */ - readonly supportsMultipleAccounts: boolean; - /** * An [event](#Event) which fires when the array of sessions has changed, or data * within a session has changed. @@ -85,31 +66,48 @@ declare module 'vscode' { /** * Returns an array of current sessions. */ + // eslint-disable-next-line vscode-dts-provider-naming getSessions(): Thenable>; /** * Prompts a user to login. */ + // eslint-disable-next-line vscode-dts-provider-naming login(scopes: string[]): Thenable; /** * Removes the session corresponding to session id. * @param sessionId The session id to log out of */ + // eslint-disable-next-line vscode-dts-provider-naming logout(sessionId: string): Thenable; } + /** + * Options for creating an [AuthenticationProvider](#AuthentcationProvider). + */ + export interface AuthenticationProviderOptions { + /** + * Whether it is possible to be signed into multiple accounts at once with this provider. + * If not specified, will default to false. + */ + readonly supportsMultipleAccounts?: boolean; + } + export namespace authentication { /** * Register an authentication provider. * * There can only be one provider per id and an error is being thrown when an id - * has already been used by another provider. + * has already been used by another provider. Ids are case-sensitive. * + * @param id The unique identifier of the provider. + * @param label The human-readable name of the provider. * @param provider The authentication provider provider. + * @params options Additional options for the provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ - export function registerAuthenticationProvider(provider: AuthenticationProvider): Disposable; + export function registerAuthenticationProvider(id: string, label: string, provider: AuthenticationProvider, options?: AuthenticationProviderOptions): Disposable; /** * @deprecated - getSession should now trigger extension activation. @@ -117,63 +115,32 @@ declare module 'vscode' { */ export const onDidChangeAuthenticationProviders: Event; - /** - * @deprecated - * The ids of the currently registered authentication providers. - * @returns An array of the ids of authentication providers that are currently registered. - */ - export function getProviderIds(): Thenable>; - - /** - * @deprecated - * An array of the ids of authentication providers that are currently registered. - */ - export const providerIds: ReadonlyArray; - /** * An array of the information of authentication providers that are currently registered. */ export const providers: ReadonlyArray; /** - * @deprecated * Logout of a specific session. * @param providerId The id of the provider to use * @param sessionId The session id to remove * provider */ export function logout(providerId: string, sessionId: string): Thenable; - - /** - * Retrieve a password that was stored with key. Returns undefined if there - * is no password matching that key. - * @param key The key the password was stored under. - */ - export function getPassword(key: string): Thenable; - - /** - * Store a password under a given key. - * @param key The key to store the password under - * @param value The password - */ - export function setPassword(key: string, value: string): Thenable; - - /** - * Remove a password from storage. - * @param key The key the password was stored under. - */ - export function deletePassword(key: string): Thenable; - - /** - * Fires when a password is set or deleted. - */ - export const onDidChangePassword: Event; } //#endregion + // eslint-disable-next-line vscode-dts-region-comments //#region @alexdima - resolvers + export interface MessageOptions { + /** + * Do not render a native message box. + */ + useCustom?: boolean; + } + export interface RemoteAuthorityResolverContext { resolveAttempt: number; } @@ -195,18 +162,20 @@ declare module 'vscode' { // The desired local port. If this port can't be used, then another will be chosen. localAddressPort?: number; label?: string; + public?: boolean; } export interface TunnelDescription { remoteAddress: { port: number, host: string; }; //The complete local address(ex. localhost:1234) localAddress: { port: number, host: string; } | string; + public?: boolean; } export interface Tunnel extends TunnelDescription { // Implementers of Tunnel should fire onDidDispose when dispose is called. onDidDispose: Event; - dispose(): void; + dispose(): void | Thenable; } /** @@ -251,10 +220,19 @@ declare module 'vscode' { */ tunnelFactory?: (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions) => Thenable | undefined; - /** + /**p * Provides filtering for candidate ports. */ showCandidatePort?: (host: string, port: number, detail: string) => Thenable; + + /** + * Lets the resolver declare which tunnel factory features it supports. + * UNDER DISCUSSION! MAY CHANGE SOON. + */ + tunnelFeatures?: { + elevation: boolean; + public: boolean; + }; } export namespace workspace { @@ -751,6 +729,7 @@ declare module 'vscode' { //#endregion + // eslint-disable-next-line vscode-dts-region-comments //#region debug /** @@ -771,8 +750,7 @@ declare module 'vscode' { //#endregion - - + // eslint-disable-next-line vscode-dts-region-comments //#region @joaomoreno: SCM validation /** @@ -823,6 +801,7 @@ declare module 'vscode' { //#endregion + // eslint-disable-next-line vscode-dts-region-comments //#region @joaomoreno: SCM selected provider export interface SourceControl { @@ -898,6 +877,7 @@ declare module 'vscode' { //#endregion + // eslint-disable-next-line vscode-dts-region-comments //#region @jrieken -> exclusive document filters export interface DocumentFilter { @@ -906,18 +886,9 @@ declare module 'vscode' { //#endregion - //#region @alexdima - OnEnter enhancement - export interface OnEnterRule { - /** - * This rule will only execute if the text above the this line matches this regular expression. - */ - oneLineAboveText?: RegExp; - } - //#endregion - - //#region Tree View: https://github.com/microsoft/vscode/issues/61313 + //#region Tree View: https://github.com/microsoft/vscode/issues/61313 @alexr00 export interface TreeView extends Disposable { - reveal(element: T | undefined, options?: { select?: boolean, focus?: boolean, expand?: boolean | number }): Thenable; + reveal(element: T | undefined, options?: { select?: boolean, focus?: boolean, expand?: boolean | number; }): Thenable; } //#endregion @@ -1001,6 +972,7 @@ declare module 'vscode' { * * @return Thenable indicating that the webview editor has been moved. */ + // eslint-disable-next-line vscode-dts-provider-naming moveCustomTextEditor?(newDocument: TextDocument, existingWebviewPanel: WebviewPanel, token: CancellationToken): Thenable; } @@ -1017,7 +989,7 @@ declare module 'vscode' { //#endregion - //#region @rebornix: Notebook + //#region notebook https://github.com/microsoft/vscode/issues/106744 export enum CellKind { Markdown = 1, @@ -1055,7 +1027,7 @@ declare module 'vscode' { /** * Additional attributes of a cell metadata. */ - custom?: { [key: string]: any }; + custom?: { [key: string]: any; }; } export interface CellDisplayOutput { @@ -1179,7 +1151,7 @@ declare module 'vscode' { /** * Additional attributes of a cell metadata. */ - custom?: { [key: string]: any }; + custom?: { [key: string]: any; }; } export interface NotebookCell { @@ -1229,7 +1201,7 @@ declare module 'vscode' { /** * Additional attributes of the document metadata. */ - custom?: { [key: string]: any }; + custom?: { [key: string]: any; }; /** * The document's current run state @@ -1241,6 +1213,11 @@ declare module 'vscode' { * When false, insecure outputs like HTML, JavaScript, SVG will not be rendered. */ trusted?: boolean; + + /** + * Languages the document supports + */ + languages?: string[]; } export interface NotebookDocumentContentOptions { @@ -1286,7 +1263,7 @@ declare module 'vscode' { locationAt(positionOrRange: Position | Range): Location; positionAt(location: Location): Position; - contains(uri: Uri): boolean + contains(uri: Uri): boolean; } export interface WorkspaceEdit { @@ -1320,11 +1297,17 @@ declare module 'vscode' { * The range will always be revealed in the center of the viewport. */ InCenter = 1, + /** * If the range is outside the viewport, it will be revealed in the center of the viewport. * Otherwise, it will be revealed with as little scrolling as possible. */ InCenterIfOutsideViewport = 2, + + /** + * The range will always be revealed at the top of the viewport. + */ + AtTop = 3 } export interface NotebookEditor { @@ -1589,11 +1572,17 @@ declare module 'vscode' { * Content providers should always use [file system providers](#FileSystemProvider) to * resolve the raw content for `uri` as the resouce is not necessarily a file on disk. */ - openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext): NotebookData | Promise; - resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Promise; - saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise; - saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise; - backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Promise; + // eslint-disable-next-line vscode-dts-provider-naming + openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext): NotebookData | Thenable; + // eslint-disable-next-line vscode-dts-provider-naming + // eslint-disable-next-line vscode-dts-cancellation + resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Thenable; + // eslint-disable-next-line vscode-dts-provider-naming + saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Thenable; + // eslint-disable-next-line vscode-dts-provider-naming + saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Thenable; + // eslint-disable-next-line vscode-dts-provider-naming + backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Thenable; } export interface NotebookKernel { @@ -1609,7 +1598,7 @@ declare module 'vscode' { cancelAllCellsExecution(document: NotebookDocument): void; } - export type NotebookFilenamePattern = GlobPattern | { include: GlobPattern; exclude: GlobPattern }; + export type NotebookFilenamePattern = GlobPattern | { include: GlobPattern; exclude: GlobPattern; }; export interface NotebookDocumentFilter { viewType?: string | string[]; @@ -1692,7 +1681,7 @@ declare module 'vscode' { ): Disposable; export function createNotebookEditorDecorationType(options: NotebookDecorationRenderOptions): NotebookEditorDecorationType; - export function openNotebookDocument(uri: Uri, viewType?: string): Promise; + export function openNotebookDocument(uri: Uri, viewType?: string): Thenable; export const onDidOpenNotebookDocument: Event; export const onDidCloseNotebookDocument: Event; export const onDidSaveNotebookDocument: Event; @@ -1715,7 +1704,7 @@ declare module 'vscode' { */ export function createConcatTextDocument(notebook: NotebookDocument, selector?: DocumentSelector): NotebookConcatTextDocument; - export const onDidChangeActiveNotebookKernel: Event<{ document: NotebookDocument, kernel: NotebookKernel | undefined }>; + export const onDidChangeActiveNotebookKernel: Event<{ document: NotebookDocument, kernel: NotebookKernel | undefined; }>; /** * Creates a notebook cell status bar [item](#NotebookCellStatusBarItem). @@ -1736,7 +1725,7 @@ declare module 'vscode' { export const onDidChangeActiveNotebookEditor: Event; export const onDidChangeNotebookEditorSelection: Event; export const onDidChangeNotebookEditorVisibleRanges: Event; - export function showNotebookDocument(document: NotebookDocument, options?: NotebookDocumentShowOptions): Promise; + export function showNotebookDocument(document: NotebookDocument, options?: NotebookDocumentShowOptions): Thenable; } //#endregion @@ -1947,11 +1936,88 @@ declare module 'vscode' { } export namespace languages { - export function getTokenInformationAtPosition(document: TextDocument, position: Position): Promise; + export function getTokenInformationAtPosition(document: TextDocument, position: Position): Thenable; } //#endregion + //#region https://github.com/microsoft/vscode/issues/16221 + + export namespace languages { + /** + * Register a inline hints provider. + * + * Multiple providers can be registered for a language. In that case providers are asked in + * parallel and the results are merged. A failing provider (rejected promise or exception) will + * not cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An inline hints provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerInlineHintsProvider(selector: DocumentSelector, provider: InlineHintsProvider): Disposable; + } + + /** + * Inline hint information. + */ + export class InlineHint { + /** + * The text of the hint. + */ + text: string; + /** + * The range of the hint. + */ + range: Range; + /** + * Tooltip when hover on the hint. + */ + description?: string | MarkdownString; + /** + * Whitespace before the hint. + */ + whitespaceBefore?: boolean; + /** + * Whitespace after the hint. + */ + whitespaceAfter?: boolean; + + /** + * Creates a new inline hint information object. + * + * @param text The text of the hint. + * @param range The range of the hint. + * @param hoverMessage Tooltip when hover on the hint. + * @param whitespaceBefore Whitespace before the hint. + * @param whitespaceAfter TWhitespace after the hint. + */ + constructor(text: string, range: Range, description?: string | MarkdownString, whitespaceBefore?: boolean, whitespaceAfter?: boolean); + } + + /** + * The inline hints provider interface defines the contract between extensions and + * the inline hints feature. + */ + export interface InlineHintsProvider { + + /** + * An optional event to signal that inline hints have changed. + * @see [EventEmitter](#EventEmitter) + */ + onDidChangeInlineHints?: Event; + + /** + * @param model The document in which the command was invoked. + * @param range The range for which line hints should be computed. + * @param token A cancellation token. + * + * @return A list of arguments labels or a thenable that resolves to such. + */ + provideInlineHints(model: TextDocument, range: Range, token: CancellationToken): ProviderResult; + } + //#endregion + //#region https://github.com/microsoft/vscode/issues/104436 export enum ExtensionRuntime { @@ -1971,7 +2037,6 @@ declare module 'vscode' { //#endregion - //#region https://github.com/microsoft/vscode/issues/102091 export interface TextDocument { @@ -2004,7 +2069,7 @@ declare module 'vscode' { * Runs tests with the given options. If no options are given, then * all tests are run. Returns the resulting test run. */ - export function runTests(options: TestRunOptions): Thenable; + export function runTests(options: TestRunOptions, cancellationToken?: CancellationToken): Thenable; /** * Returns an observer that retrieves tests in the given workspace folder. @@ -2015,13 +2080,31 @@ declare module 'vscode' { * Returns an observer that retrieves tests in the given text document. */ export function createDocumentTestObserver(document: TextDocument): TestObserver; + + /** + * The last or selected test run. Cleared when a new test run starts. + */ + export const testResults: TestResults | undefined; + + /** + * Event that fires when the testResults are updated. + */ + export const onDidChangeTestResults: Event; + } + + export interface TestResults { + /** + * The results from the latest test run. The array contains a snapshot of + * all tests involved in the run at the moment when it completed. + */ + readonly tests: ReadonlyArray | undefined; } export interface TestObserver { /** * List of tests returned by test provider for files in the workspace. */ - readonly tests: ReadonlyArray; + readonly tests: ReadonlyArray; /** * An event that fires when an existing test in the collection changes, or @@ -2051,23 +2134,23 @@ declare module 'vscode' { /** * List of all tests that are newly added. */ - readonly added: ReadonlyArray; + readonly added: ReadonlyArray; /** * List of existing tests that have updated. */ - readonly updated: ReadonlyArray; + readonly updated: ReadonlyArray; /** * List of existing tests that have been removed. */ - readonly removed: ReadonlyArray; + readonly removed: ReadonlyArray; /** * Highest node in the test tree under which changes were made. This can * be easily plugged into events like the TreeDataProvider update event. */ - readonly commonChangeAncestor: TestItem | null; + readonly commonChangeAncestor: RequiredTestItem | null; } /** @@ -2094,13 +2177,11 @@ declare module 'vscode' { readonly onDidChangeTest: Event; /** - * An event that should be fired when all tests that are currently defined - * have been discovered. The provider should continue to watch for changes - * and fire `onDidChangeTest` until the hierarchy is disposed. - * - * @todo can this be covered by existing progress apis? Or return a promise + * Promise that should be resolved when all tests that are initially + * defined have been discovered. The provider should continue to watch for + * changes and fire `onDidChangeTest` until the hierarchy is disposed. */ - readonly onDidDiscoverInitialTests: Event; + readonly discoveredInitialTests?: Thenable; /** * Dispose will be called when there are no longer observers interested @@ -2129,20 +2210,23 @@ declare module 'vscode' { * It's guaranteed that this method will not be called again while * there is a previous undisposed watcher for the given workspace folder. */ - createWorkspaceTestHierarchy?(workspace: WorkspaceFolder): TestHierarchy; + // eslint-disable-next-line vscode-dts-provider-naming + createWorkspaceTestHierarchy?(workspace: WorkspaceFolder): TestHierarchy | undefined; /** * Requests that tests be provided for the given document. This will * be called when tests need to be enumerated for a single open file, * for instance by code lens UI. */ - createDocumentTestHierarchy?(document: TextDocument): TestHierarchy; + // eslint-disable-next-line vscode-dts-provider-naming + createDocumentTestHierarchy?(document: TextDocument): TestHierarchy | undefined; /** * Starts a test run. This should cause {@link onDidChangeTest} to * fire with update test states during the run. * @todo this will eventually need to be able to return a summary report, coverage for example. */ + // eslint-disable-next-line vscode-dts-provider-naming runTests?(options: TestRunOptions, cancellationToken: CancellationToken): ProviderResult; } @@ -2172,6 +2256,16 @@ declare module 'vscode' { */ label: string; + /** + * Optional unique identifier for the TestItem. This is used to correlate + * test results and tests in the document with those in the workspace + * (test explorer). This must not change for the lifetime of a test item. + * + * If the ID is not provided, it defaults to the concatenation of the + * item's label and its parent's ID, if any. + */ + readonly id?: string; + /** * Optional description that appears next to the label. */ @@ -2208,19 +2302,30 @@ declare module 'vscode' { state: TestState; } + /** + * A {@link TestItem} with its defaults filled in. + */ + export type RequiredTestItem = { + [K in keyof Required]: K extends 'children' + ? RequiredTestItem[] + : (K extends 'description' | 'location' ? TestItem[K] : Required[K]) + }; + export enum TestRunState { // Initial state Unset = 0, + // Test will be run, but is not currently running. + Queued = 1, // Test is currently running - Running = 1, + Running = 2, // Test run has passed - Passed = 2, + Passed = 3, // Test run has failed (on an assertion) - Failed = 3, + Failed = 4, // Test run has been skipped - Skipped = 4, + Skipped = 5, // Test run failed for some other reason (compilation error, timeout, etc) - Errored = 5 + Errored = 6 } /** @@ -2294,27 +2399,180 @@ declare module 'vscode' { */ location?: Location; } + //#endregion - //#region Statusbar Item Background Color (https://github.com/microsoft/vscode/issues/110214) + //#region Opener service (https://github.com/microsoft/vscode/issues/109277) /** - * A status bar item is a status bar contribution that can - * show text and icons and run a command on click. + * Details if an `ExternalUriOpener` can open a uri. + * + * The priority is also used to rank multiple openers against each other and determine + * if an opener should be selected automatically or if the user should be prompted to + * select an opener. + * + * VS Code will try to use the best available opener, as sorted by `ExternalUriOpenerPriority`. + * If there are multiple potential "best" openers for a URI, then the user will be prompted + * to select an opener. */ - export interface StatusBarItem { + export enum ExternalUriOpenerPriority { + /** + * The opener is disabled and will never be shown to users. + * + * Note that the opener can still be used if the user specifically + * configures it in their settings. + */ + None = 0, /** - * The background color for this entry. - * - * Note: only `new ThemeColor('statusBarItem.errorBackground')` is - * supported for now. More background colors may be supported in the - * future. - * - * Note: when a background color is set, the statusbar may override - * the `color` choice to ensure the entry is readable in all themes. + * The opener can open the uri but will not cause a prompt on its own + * since VS Code always contributes a built-in `Default` opener. */ - backgroundColor: ThemeColor | undefined; + Option = 1, + + /** + * The opener can open the uri. + * + * VS Code's built-in opener has `Default` priority. This means that any additional `Default` + * openers will cause the user to be prompted to select from a list of all potential openers. + */ + Default = 2, + + /** + * The opener can open the uri and should be automatically selected over any + * default openers, include the built-in one from VS Code. + * + * A preferred opener will be automatically selected if no other preferred openers + * are available. If multiple preferred openers are available, then the user + * is shown a prompt with all potential openers (not just preferred openers). + */ + Preferred = 3, + } + + /** + * Handles opening uris to external resources, such as http(s) links. + * + * Extensions can implement an `ExternalUriOpener` to open `http` links to a webserver + * inside of VS Code instead of having the link be opened by the web browser. + * + * Currently openers may only be registered for `http` and `https` uris. + */ + export interface ExternalUriOpener { + + /** + * Check if the opener can open a uri. + * + * @param uri The uri being opened. This is the uri that the user clicked on. It has + * not yet gone through port forwarding. + * @param token Cancellation token indicating that the result is no longer needed. + * + * @return Priority indicating if the opener can open the external uri. + */ + canOpenExternalUri(uri: Uri, token: CancellationToken): ExternalUriOpenerPriority | Thenable; + + /** + * Open a uri. + * + * This is invoked when: + * + * - The user clicks a link which does not have an assigned opener. In this case, first `canOpenExternalUri` + * is called and if the user selects this opener, then `openExternalUri` is called. + * - The user sets the default opener for a link in their settings and then visits a link. + * + * @param resolvedUri The uri to open. This uri may have been transformed by port forwarding, so it + * may not match the original uri passed to `canOpenExternalUri`. Use `ctx.originalUri` to check the + * original uri. + * @param ctx Additional information about the uri being opened. + * @param token Cancellation token indicating that opening has been canceled. + * + * @return Thenable indicating that the opening has completed. + */ + openExternalUri(resolvedUri: Uri, ctx: OpenExternalUriContext, token: CancellationToken): Thenable | void; + } + + /** + * Additional information about the uri being opened. + */ + interface OpenExternalUriContext { + /** + * The uri that triggered the open. + * + * This is the original uri that the user clicked on or that was passed to `openExternal.` + * Due to port forwarding, this may not match the `resolvedUri` passed to `openExternalUri`. + */ + readonly sourceUri: Uri; + } + + /** + * Additional metadata about a registered `ExternalUriOpener`. + */ + interface ExternalUriOpenerMetadata { + + /** + * List of uri schemes the opener is triggered for. + * + * Currently only `http` and `https` are supported. + */ + readonly schemes: readonly string[] + + /** + * Text displayed to the user that explains what the opener does. + * + * For example, 'Open in browser preview' + */ + readonly label: string; + } + + namespace window { + /** + * Register a new `ExternalUriOpener`. + * + * When a uri is about to be opened, an `onUriOpen:SCHEME` activation event is fired. + * + * @param id Unique id of the opener, such as `myExtension.browserPreview`. This is used in settings + * and commands to identify the opener. + * @param opener Opener to register. + * @param metadata Additional information about the opener. + * + * @returns Disposable that unregisters the opener. + */ + export function registerExternalUriOpener(id: string, opener: ExternalUriOpener, metadata: ExternalUriOpenerMetadata): Disposable; + } + + interface OpenExternalOptions { + /** + * Allows using openers contributed by extensions through `registerExternalUriOpener` + * when opening the resource. + * + * If `true`, VS Code will check if any contributed openers can handle the + * uri, and fallback to the default opener behavior. + * + * If it is string, this specifies the id of the `ExternalUriOpener` + * that should be used if it is available. Use `'default'` to force VS Code's + * standard external opener to be used. + */ + readonly allowContributedOpeners?: boolean | string; + } + + namespace env { + export function openExternal(target: Uri, options?: OpenExternalOptions): Thenable; + } + + //endregionn + + //#region https://github.com/Microsoft/vscode/issues/15178 + + // TODO@API must be a class + export interface OpenEditorInfo { + name: string; + resource: Uri; + } + + export namespace window { + export const openEditors: ReadonlyArray; + + // todo@API proper event type + export const onDidChangeOpenEditors: Event; } //#endregion diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 3b4c8a66c..1e3169fd5 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -17,6 +17,7 @@ import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEdito // --- mainThread participants import './mainThreadBulkEdits'; import './mainThreadCodeInsets'; +import './mainThreadCLICommands'; import './mainThreadClipboard'; import './mainThreadCommands'; import './mainThreadConfiguration'; @@ -30,6 +31,7 @@ import './mainThreadDocuments'; import './mainThreadDocumentsAndEditors'; import './mainThreadEditor'; import './mainThreadEditors'; +import './mainThreadEditorTabs'; import './mainThreadErrors'; import './mainThreadExtensionService'; import './mainThreadFileSystem'; @@ -54,6 +56,7 @@ import './mainThreadTheming'; import './mainThreadTreeViews'; import './mainThreadDownloadService'; import './mainThreadUrls'; +import './mainThreadUriOpeners'; import './mainThreadWindow'; import './mainThreadWebviewManager'; import './mainThreadWorkspace'; @@ -65,6 +68,7 @@ import './mainThreadTunnelService'; import './mainThreadAuthentication'; import './mainThreadTimeline'; import './mainThreadTesting'; +import './mainThreadSecretState'; import 'vs/workbench/api/common/apiCommands'; export class ExtensionPoints implements IWorkbenchContribution { diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index c39650404..98bb98d8c 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -18,9 +18,6 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { fromNow } from 'vs/base/common/date'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { isWeb } from 'vs/base/common/platform'; -import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials'; const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git', 'ms-vsonline.vsonline', 'vscode.github-browser', 'ms-vscode.github-browser', 'github.codespaces']; @@ -225,10 +222,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu @INotificationService private readonly notificationService: INotificationService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IExtensionService private readonly extensionService: IExtensionService, - @ICredentialsService private readonly credentialsService: ICredentialsService, - @IEncryptionService private readonly encryptionService: IEncryptionService, - @IProductService private readonly productService: IProductService + @IExtensionService private readonly extensionService: IExtensionService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); @@ -250,10 +244,6 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu this._register(this.authenticationService.onDidChangeDeclaredProviders(e => { this._proxy.$setProviders(e); })); - - this._register(this.credentialsService.onDidChangePassword(_ => { - this._proxy.$onDidChangePassword(); - })); } $getProviderIds(): Promise { @@ -416,7 +406,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu const remoteConnection = this.remoteAgentService.getConnection(); const isVSO = remoteConnection !== null - ? remoteConnection.remoteAuthority.startsWith('vsonline') + ? remoteConnection.remoteAuthority.startsWith('vsonline') || remoteConnection.remoteAuthority.startsWith('codespaces') : isWeb; if (isVSO && VSO_ALLOWED_EXTENSIONS.includes(extensionId)) { @@ -466,46 +456,4 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu this.storageService.store(`${extensionName}-${providerId}`, sessionId, StorageScope.GLOBAL, StorageTarget.MACHINE); addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName); } - - private getFullKey(extensionId: string): string { - return `${this.productService.urlProtocol}${extensionId}`; - } - - async $getPassword(extensionId: string, key: string): Promise { - const fullKey = this.getFullKey(extensionId); - const password = await this.credentialsService.getPassword(fullKey, key); - const decrypted = password && await this.encryptionService.decrypt(password); - - if (decrypted) { - try { - const value = JSON.parse(decrypted); - if (value.extensionId === extensionId) { - return value.content; - } - } catch (_) { - throw new Error('Cannot get password'); - } - } - - return undefined; - } - - async $setPassword(extensionId: string, key: string, value: string): Promise { - const fullKey = this.getFullKey(extensionId); - const toEncrypt = JSON.stringify({ - extensionId, - content: value - }); - const encrypted = await this.encryptionService.encrypt(toEncrypt); - return this.credentialsService.setPassword(fullKey, key, encrypted); - } - - async $deletePassword(extensionId: string, key: string): Promise { - try { - const fullKey = this.getFullKey(extensionId); - await this.credentialsService.deletePassword(fullKey, key); - } catch (_) { - throw new Error('Cannot delete password'); - } - } } diff --git a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts index 70fb045d7..8583555b8 100644 --- a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts +++ b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts @@ -3,29 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; -import { IExtHostContext, IWorkspaceEditDto, WorkspaceEditType, MainThreadBulkEditsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { revive } from 'vs/base/common/marshalling'; -import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; -import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; - -function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { - if (!data?.edits) { - return []; - } - - const result: ResourceEdit[] = []; - for (let edit of revive(data).edits) { - if (edit._type === WorkspaceEditType.File) { - result.push(new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata)); - } else if (edit._type === WorkspaceEditType.Text) { - result.push(new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata)); - } else if (edit._type === WorkspaceEditType.Cell) { - result.push(new ResourceNotebookCellEdit(edit.resource, edit.edit, edit.notebookVersionId, edit.metadata)); - } - } - return result; -} +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IExtHostContext, IWorkspaceEditDto, MainThreadBulkEditsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { reviveWorkspaceEditDto2 } from 'vs/workbench/api/browser/mainThreadEditors'; @extHostNamedCustomer(MainContext.MainThreadBulkEdits) export class MainThreadBulkEdits implements MainThreadBulkEditsShape { @@ -39,12 +19,6 @@ export class MainThreadBulkEdits implements MainThreadBulkEditsShape { $tryApplyWorkspaceEdit(dto: IWorkspaceEditDto, undoRedoGroupId?: number): Promise { const edits = reviveWorkspaceEditDto2(dto); - return this._bulkEditService.apply(edits, { - // having a undoRedoGroupId means that this is a nested workspace edit, - // e.g one from a onWill-handler and for now we need to forcefully suppress - // refactor previewing, see: https://github.com/microsoft/vscode/issues/111873#issuecomment-738739852 - undoRedoGroupId, - suppressPreview: typeof undoRedoGroupId === 'number' ? true : undefined - }).then(() => true, _err => false); + return this._bulkEditService.apply(edits, { undoRedoGroupId }).then(() => true, _err => false); } } diff --git a/src/vs/workbench/api/browser/mainThreadCLICommands.ts b/src/vs/workbench/api/browser/mainThreadCLICommands.ts new file mode 100644 index 000000000..a02cdf6ea --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadCLICommands.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Schemas } from 'vs/base/common/network'; +import { isString } from 'vs/base/common/types'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { CLIOutput, IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; +import { getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { canExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { IExtensionManifest } from 'vs/workbench/workbench.web.api'; + + +// this class contains the commands that the CLI server is reying on + +CommandsRegistry.registerCommand('_remoteCLI.openExternal', function (accessor: ServicesAccessor, uri: UriComponents, options: { allowTunneling?: boolean }) { + // TODO: discuss martin, ben where to put this + const openerService = accessor.get(IOpenerService); + openerService.open(URI.revive(uri), { openExternal: true, allowTunneling: options?.allowTunneling === true }); +}); + +interface ManageExtensionsArgs { + list?: { showVersions?: boolean, category?: string; }; + install?: (string | URI)[]; + uninstall?: string[]; + force?: boolean; +} + +CommandsRegistry.registerCommand('_remoteCLI.manageExtensions', async function (accessor: ServicesAccessor, args: ManageExtensionsArgs) { + + const instantiationService = accessor.get(IInstantiationService); + const extensionManagementServerService = accessor.get(IExtensionManagementServerService); + const remoteExtensionManagementService = extensionManagementServerService.remoteExtensionManagementServer?.extensionManagementService; + if (!remoteExtensionManagementService) { + return; + } + + const cliService = instantiationService.createChild(new ServiceCollection([IExtensionManagementService, remoteExtensionManagementService])).createInstance(RemoteExtensionCLIManagementService); + + const lines: string[] = []; + const output = { log: lines.push.bind(lines), error: lines.push.bind(lines) }; + + if (args.list) { + await cliService.listExtensions(!!args.list.showVersions, args.list.category, output); + } else { + const revive = (inputs: (string | UriComponents)[]) => inputs.map(input => isString(input) ? input : URI.revive(input)); + if (Array.isArray(args.install) && args.install.length) { + try { + await cliService.installExtensions(revive(args.install), [], true, !!args.force, output); + } catch (e) { + lines.push(e.message); + } + } + if (Array.isArray(args.uninstall) && args.uninstall.length) { + try { + await cliService.uninstallExtensions(revive(args.uninstall), !!args.force, output); + } catch (e) { + lines.push(e.message); + } + } + } + return lines.join('\n'); +}); + +class RemoteExtensionCLIManagementService extends ExtensionManagementCLIService { + + private _location: string | undefined; + + constructor( + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IProductService private readonly productService: IProductService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, + @ILocalizationsService localizationsService: ILocalizationsService, + @ILabelService labelService: ILabelService, + @IWorkbenchEnvironmentService envService: IWorkbenchEnvironmentService + ) { + super(extensionManagementService, extensionGalleryService, localizationsService); + + const remoteAuthority = envService.remoteAuthority; + this._location = remoteAuthority ? labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority) : undefined; + } + + protected get location(): string | undefined { + return this._location; + } + + protected validateExtensionKind(manifest: IExtensionManifest, output: CLIOutput): boolean { + if (!canExecuteOnWorkspace(manifest, this.productService, this.configurationService)) { + output.log(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", getExtensionId(manifest.publisher, manifest.name))); + return false; + } + return true; + } +} diff --git a/src/vs/workbench/api/browser/mainThreadConsole.ts b/src/vs/workbench/api/browser/mainThreadConsole.ts index 4a244875f..d678f21bc 100644 --- a/src/vs/workbench/api/browser/mainThreadConsole.ts +++ b/src/vs/workbench/api/browser/mainThreadConsole.ts @@ -10,22 +10,18 @@ import { IRemoteConsoleLog, log } from 'vs/base/common/console'; import { logRemoteEntry } from 'vs/workbench/services/extensions/common/remoteConsoleUtil'; import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { ILogService } from 'vs/platform/log/common/log'; -import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; @extHostNamedCustomer(MainContext.MainThreadConsole) export class MainThreadConsole implements MainThreadConsoleShape { - private readonly _isExtensionDevHost: boolean; private readonly _isExtensionDevTestFromCli: boolean; constructor( - extHostContext: IExtHostContext, + _extHostContext: IExtHostContext, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @ILogService private readonly _logService: ILogService, - @IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService, ) { const devOpts = parseExtensionDevOptions(this._environmentService); - this._isExtensionDevHost = devOpts.isExtensionDevHost; this._isExtensionDevTestFromCli = devOpts.isExtensionDevTestFromCli; } @@ -43,10 +39,5 @@ export class MainThreadConsole implements MainThreadConsoleShape { if (this._isExtensionDevTestFromCli) { logRemoteEntry(this._logService, entry); } - - // Broadcast to other windows if we are in development mode - else if (this._environmentService.debugExtensionHost.debugId && (!this._environmentService.isBuilt || this._isExtensionDevHost)) { - this._extensionHostDebugService.logToSession(this._environmentService.debugExtensionHost.debugId, entry); - } } } diff --git a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index bcf31a1a7..e5f1d3f57 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { multibyteAwareBtoa } from 'vs/base/browser/dom'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; import { isEqual, isEqualOrParent, toLocalResource } from 'vs/base/common/resources'; -import { multibyteAwareBtoa } from 'vs/base/browser/dom'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as modes from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; @@ -95,10 +95,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc dispose() { super.dispose(); - for (const disposable of this._editorProviders.values()) { - disposable.dispose(); - } - + dispose(this._editorProviders.values()); this._editorProviders.clear(); } diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index f1906f16a..cb4c39856 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -327,7 +327,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return { id: sessionID, type: session.configuration.type, - name: session.configuration.name, + name: session.name, folderUri: session.root ? session.root.uri : undefined, configuration: session.configuration }; diff --git a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts new file mode 100644 index 000000000..2e6aaffa5 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ExtHostContext, IExtHostEditorTabsShape, IExtHostContext, MainContext, IEditorTabDto } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { Verbosity } from 'vs/workbench/common/editor'; +import { GroupChangeKind, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; + +export interface ITabInfo { + name: string; + resource: URI; +} + +@extHostNamedCustomer(MainContext.MainThreadEditorTabs) +export class MainThreadEditorTabs { + + private static _GroupEventFilter = new Set([GroupChangeKind.EDITOR_CLOSE, GroupChangeKind.EDITOR_OPEN]); + + private readonly _dispoables = new DisposableStore(); + private readonly _groups = new Map(); + private readonly _proxy: IExtHostEditorTabsShape; + + constructor( + extHostContext: IExtHostContext, + @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, + ) { + + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditorTabs); + + this._editorGroupsService.groups.forEach(this._subscribeToGroup, this); + this._dispoables.add(_editorGroupsService.onDidAddGroup(this._subscribeToGroup, this)); + this._dispoables.add(_editorGroupsService.onDidRemoveGroup(e => { + const subscription = this._groups.get(e); + if (subscription) { + subscription.dispose(); + this._groups.delete(e); + this._pushEditorTabs(); + } + })); + this._pushEditorTabs(); + } + + dispose(): void { + dispose(this._groups.values()); + this._dispoables.dispose(); + } + + private _subscribeToGroup(group: IEditorGroup) { + this._groups.get(group)?.dispose(); + const listener = group.onDidGroupChange(e => { + if (MainThreadEditorTabs._GroupEventFilter.has(e.kind)) { + this._pushEditorTabs(); + } + }); + this._groups.set(group, listener); + } + + private _pushEditorTabs(): void { + const tabs: IEditorTabDto[] = []; + for (const group of this._editorGroupsService.groups) { + for (const editor of group.editors) { + if (editor.isDisposed() || !editor.resource) { + continue; + } + tabs.push({ + group: group.id, + name: editor.getTitle(Verbosity.SHORT) ?? '', + resource: editor.resource + }); + } + } + + this._proxy.$acceptEditorTabs(tabs); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 1723f6328..0f52d9af1 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -27,7 +27,7 @@ import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/wo import { revive } from 'vs/base/common/marshalling'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; -function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { +export function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { if (!data?.edits) { return []; } diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 5d0132e29..11cfcc3db 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -7,7 +7,7 @@ import { SerializedError } from 'vs/base/common/errors'; import Severity from 'vs/base/common/severity'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { IExtHostContext, MainContext, MainThreadExtensionServiceShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IExtensionService, ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, ExtensionActivationError, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { localize } from 'vs/nls'; @@ -19,29 +19,23 @@ import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/com import { CancellationToken } from 'vs/base/common/cancellation'; import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; +import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; @extHostNamedCustomer(MainContext.MainThreadExtensionService) export class MainThreadExtensionService implements MainThreadExtensionServiceShape { - private readonly _extensionService: IExtensionService; - private readonly _notificationService: INotificationService; - private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService; - private readonly _hostService: IHostService; - private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService; + private readonly _extensionHostKind: ExtensionHostKind; constructor( extHostContext: IExtHostContext, - @IExtensionService extensionService: IExtensionService, - @INotificationService notificationService: INotificationService, - @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, - @IHostService hostService: IHostService, - @IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService + @IExtensionService private readonly _extensionService: IExtensionService, + @INotificationService private readonly _notificationService: INotificationService, + @IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService, + @IHostService private readonly _hostService: IHostService, + @IWorkbenchExtensionEnablementService private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService, + @ITimerService private readonly _timerService: ITimerService, ) { - this._extensionService = extensionService; - this._notificationService = notificationService; - this._extensionsWorkbenchService = extensionsWorkbenchService; - this._hostService = hostService; - this._extensionEnablementService = extensionEnablementService; + this._extensionHostKind = extHostContext.extensionHostKind; } public dispose(): void { @@ -131,4 +125,14 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha async $onExtensionHostExit(code: number): Promise { this._extensionService._onExtensionHostExit(code); } + + async $setPerformanceMarks(marks: PerformanceMark[]): Promise { + if (this._extensionHostKind === ExtensionHostKind.LocalProcess) { + this._timerService.setPerformanceMarks('localExtHost', marks); + } else if (this._extensionHostKind === ExtensionHostKind.LocalWebWorker) { + this._timerService.setPerformanceMarks('workerExtHost', marks); + } else { + this._timerService.setPerformanceMarks('remoteExtHost', marks); + } + } } diff --git a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts index 81905c6b1..c03943332 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts +++ b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts @@ -4,23 +4,43 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore } from 'vs/base/common/lifecycle'; -import { FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { FileChangeType, FileOperation, IFileService } from 'vs/platform/files/common/files'; import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, FileSystemEvents, IExtHostContext } from '../common/extHost.protocol'; import { localize } from 'vs/nls'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IWorkingCopyFileOperationParticipant, IWorkingCopyFileService, SourceTargetPair, IFileOperationUndoRedoInfo } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { reviveWorkspaceEditDto2 } from 'vs/workbench/api/browser/mainThreadEditors'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { raceCancellation } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import Severity from 'vs/base/common/severity'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @extHostCustomer export class MainThreadFileSystemEventService { + static readonly MementoKeyAdditionalEdits = `file.particpants.additionalEdits`; + private readonly _listener = new DisposableStore(); constructor( extHostContext: IExtHostContext, @IFileService fileService: IFileService, - @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService + @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, + @IBulkEditService bulkEditService: IBulkEditService, + @IProgressService progressService: IProgressService, + @IDialogService dialogService: IDialogService, + @IStorageService storageService: IStorageService, + @ILogService logService: ILogService, + @IEnvironmentService envService: IEnvironmentService ) { const proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService); @@ -53,14 +73,124 @@ export class MainThreadFileSystemEventService { })); - // BEFORE file operation - this._listener.add(workingCopyFileService.addFileOperationParticipant({ - participate: async (files, operation, undoRedoGroupId, isUndoing, _progress, timeout, token) => { - if (!isUndoing) { - return proxy.$onWillRunFileOperation(operation, files, undoRedoGroupId, timeout, token); + const fileOperationParticipant = new class implements IWorkingCopyFileOperationParticipant { + async participate(files: SourceTargetPair[], operation: FileOperation, undoInfo: IFileOperationUndoRedoInfo | undefined, timeout: number, token: CancellationToken) { + if (undoInfo?.isUndoing) { + return; + } + + const cts = new CancellationTokenSource(token); + const timer = setTimeout(() => cts.cancel(), timeout); + + const data = await progressService.withProgress({ + location: ProgressLocation.Notification, + title: this._progressLabel(operation), + cancellable: true, + delay: Math.min(timeout / 2, 3000) + }, () => { + // race extension host event delivery against timeout AND user-cancel + const onWillEvent = proxy.$onWillRunFileOperation(operation, files, timeout, token); + return raceCancellation(onWillEvent, cts.token); + }, () => { + // user-cancel + cts.cancel(); + + }).finally(() => { + cts.dispose(); + clearTimeout(timer); + }); + + if (!data) { + // cancelled or no reply + return; + } + + const needsConfirmation = data.edit.edits.some(edit => edit.metadata?.needsConfirmation); + let showPreview = storageService.getBoolean(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.GLOBAL); + + if (envService.extensionTestsLocationURI) { + // don't show dialog in tests + showPreview = false; + } + + if (showPreview === undefined) { + // show a user facing message + + let message: string; + if (data.extensionNames.length === 1) { + if (operation === FileOperation.CREATE) { + message = localize('ask.1.create', "Extension '{0}' wants to make refactoring changes with this file creation", data.extensionNames[0]); + } else if (operation === FileOperation.COPY) { + message = localize('ask.1.copy', "Extension '{0}' wants to make refactoring changes with this file copy", data.extensionNames[0]); + } else if (operation === FileOperation.MOVE) { + message = localize('ask.1.move', "Extension '{0}' wants to make refactoring changes with this file move", data.extensionNames[0]); + } else /* if (operation === FileOperation.DELETE) */ { + message = localize('ask.1.delete', "Extension '{0}' wants to make refactoring changes with this file deletion", data.extensionNames[0]); + } + } else { + if (operation === FileOperation.CREATE) { + message = localize('ask.N.create', "{0} extensions want to make refactoring changes with this file creation", data.extensionNames.length); + } else if (operation === FileOperation.COPY) { + message = localize('ask.N.copy', "{0} extensions want to make refactoring changes with this file copy", data.extensionNames.length); + } else if (operation === FileOperation.MOVE) { + message = localize('ask.N.move', "{0} extensions want to make refactoring changes with this file move", data.extensionNames.length); + } else /* if (operation === FileOperation.DELETE) */ { + message = localize('ask.N.delete', "{0} extensions want to make refactoring changes with this file deletion", data.extensionNames.length); + } + } + + if (needsConfirmation) { + // edit which needs confirmation -> always show dialog + const answer = await dialogService.show(Severity.Info, message, [localize('preview', "Show Preview"), localize('cancel', "Skip Changes")], { cancelId: 1 }); + showPreview = true; + if (answer.choice === 1) { + // no changes wanted + return; + } + } else { + // choice + const answer = await dialogService.show(Severity.Info, message, + [localize('ok', "OK"), localize('preview', "Show Preview"), localize('cancel', "Skip Changes")], + { + cancelId: 2, + checkbox: { label: localize('again', "Don't ask again") } + } + ); + if (answer.choice === 2) { + // no changes wanted, don't persist cancel option + return; + } + showPreview = answer.choice === 1; + if (answer.checkboxChecked /* && answer.choice !== 2 */) { + storageService.store(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, showPreview, StorageScope.GLOBAL, StorageTarget.USER); + } + } + } + + logService.info('[onWill-handler] applying additional workspace edit from extensions', data.extensionNames); + + await bulkEditService.apply( + reviveWorkspaceEditDto2(data.edit), + { undoRedoGroupId: undoInfo?.undoRedoGroupId, showPreview } + ); + } + + private _progressLabel(operation: FileOperation): string { + switch (operation) { + case FileOperation.CREATE: + return localize('msg-create', "Running 'File Create' participants..."); + case FileOperation.MOVE: + return localize('msg-rename', "Running 'File Rename' participants..."); + case FileOperation.COPY: + return localize('msg-copy', "Running 'File Copy' participants..."); + case FileOperation.DELETE: + return localize('msg-delete', "Running 'File Delete' participants..."); } } - })); + }; + + // BEFORE file operation + this._listener.add(workingCopyFileService.addFileOperationParticipant(fileOperationParticipant)); // AFTER file operation this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => proxy.$onDidRunFileOperation(e.operation, e.files))); @@ -71,6 +201,19 @@ export class MainThreadFileSystemEventService { } } +registerAction2(class ResetMemento extends Action2 { + constructor() { + super({ + id: 'files.participants.resetChoice', + title: localize('label', "Reset choice for 'File operation needs preview'"), + f1: true + }); + } + run(accessor: ServicesAccessor) { + accessor.get(IStorageService).remove(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.GLOBAL); + } +}); + Registry.as(Extensions.Configuration).registerConfiguration({ id: 'files', diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index ddea8e0e9..391cc7add 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -21,7 +21,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { mixin } from 'vs/base/common/objects'; -import { decodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto'; +import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape { @@ -497,6 +497,32 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha })); } + // --- inline hints + + $registerInlineHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void { + const provider = { + provideInlineHints: async (model: ITextModel, range: EditorRange, token: CancellationToken): Promise => { + const result = await this._proxy.$provideInlineHints(handle, model.uri, range, token); + return result?.hints; + } + }; + + if (typeof eventHandle === 'number') { + const emitter = new Emitter(); + this._registrations.set(eventHandle, emitter); + provider.onDidChangeInlineHints = emitter.event; + } + + this._registrations.set(handle, modes.InlineHintsProviderRegistry.register(selector, provider)); + } + + $emitInlineHintsEvent(eventHandle: number, event?: any): void { + const obj = this._registrations.get(eventHandle); + if (obj instanceof Emitter) { + obj.fire(event); + } + } + // --- links $registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void { @@ -662,7 +688,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha return { beforeText: MainThreadLanguageFeatures._reviveRegExp(onEnterRule.beforeText), afterText: onEnterRule.afterText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.afterText) : undefined, - oneLineAboveText: onEnterRule.oneLineAboveText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.oneLineAboveText) : undefined, + previousLineText: onEnterRule.previousLineText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.previousLineText) : undefined, action: onEnterRule.action }; } diff --git a/src/vs/workbench/api/browser/mainThreadMessageService.ts b/src/vs/workbench/api/browser/mainThreadMessageService.ts index c956da47b..b7470ac8f 100644 --- a/src/vs/workbench/api/browser/mainThreadMessageService.ts +++ b/src/vs/workbench/api/browser/mainThreadMessageService.ts @@ -33,7 +33,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape { $showMessage(severity: Severity, message: string, options: MainThreadMessageOptions, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Promise { if (options.modal) { - return this._showModalMessage(severity, message, commands); + return this._showModalMessage(severity, message, commands, options.useCustom); } else { return this._showMessage(severity, message, commands, options.extension); } @@ -97,7 +97,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape { }); } - private async _showModalMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Promise { + private async _showModalMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[], useCustom?: boolean): Promise { let cancelId: number | undefined = undefined; const buttons = commands.map((command, index) => { @@ -118,7 +118,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape { cancelId = buttons.length - 1; } - const { choice } = await this._dialogService.show(severity, message, buttons, { cancelId }); + const { choice } = await this._dialogService.show(severity, message, buttons, { cancelId, useCustom }); return choice === commands.length ? undefined : commands[choice].handle; } } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index cbf34422d..0b4b7f3f5 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -16,8 +16,8 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EditorActivation, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { viewColumnToEditorGroup } from 'vs/workbench/common/editor'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -129,12 +129,12 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @IQuickInputService private readonly quickInputService: IQuickInputService, @ILogService private readonly logService: ILogService, @INotebookCellStatusBarService private readonly cellStatusBarService: INotebookCellStatusBarService, @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); @@ -646,14 +646,13 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo switch (revealType) { case NotebookEditorRevealType.Default: - notebookEditor.revealInView(cell); - break; + return notebookEditor.revealCellRangeInView(range); case NotebookEditorRevealType.InCenter: - notebookEditor.revealInCenter(cell); - break; + return notebookEditor.revealInCenter(cell); case NotebookEditorRevealType.InCenterIfOutsideViewport: - notebookEditor.revealInCenterIfOutsideViewport(cell); - break; + return notebookEditor.revealInCenterIfOutsideViewport(cell); + case NotebookEditorRevealType.AtTop: + return notebookEditor.revealInViewAtTop(cell); default: break; } @@ -733,7 +732,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo const input = this.editorService.createEditorInput({ resource: URI.revive(resource), options: editorOptions }); // TODO: handle options.selection - const editorPane = await openEditorWith(input, viewType, options, group, this.editorService, this.configurationService, this.quickInputService); + const editorPane = await this._instantiationService.invokeFunction(openEditorWith, input, viewType, options, group); const notebookEditor = (editorPane as unknown as { isNotebookEditor?: boolean })?.isNotebookEditor ? (editorPane!.getControl() as INotebookEditor) : undefined; if (notebookEditor) { diff --git a/src/vs/workbench/api/browser/mainThreadSecretState.ts b/src/vs/workbench/api/browser/mainThreadSecretState.ts new file mode 100644 index 000000000..4067acedb --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadSecretState.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials'; +import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService'; +import { ExtHostContext, ExtHostSecretStateShape, IExtHostContext, MainContext, MainThreadSecretStateShape } from '../common/extHost.protocol'; + +@extHostNamedCustomer(MainContext.MainThreadSecretState) +export class MainThreadSecretState extends Disposable implements MainThreadSecretStateShape { + private readonly _proxy: ExtHostSecretStateShape; + + constructor( + extHostContext: IExtHostContext, + @ICredentialsService private readonly credentialsService: ICredentialsService, + @IEncryptionService private readonly encryptionService: IEncryptionService, + @IProductService private readonly productService: IProductService + ) { + super(); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSecretState); + + this._register(this.credentialsService.onDidChangePassword(e => { + const extensionId = e.service.substring(this.productService.urlProtocol.length); + this._proxy.$onDidChangePassword({ extensionId, key: e.account }); + })); + } + + private getFullKey(extensionId: string): string { + return `${this.productService.urlProtocol}${extensionId}`; + } + + async $getPassword(extensionId: string, key: string): Promise { + const fullKey = this.getFullKey(extensionId); + const password = await this.credentialsService.getPassword(fullKey, key); + const decrypted = password && await this.encryptionService.decrypt(password); + + if (decrypted) { + try { + const value = JSON.parse(decrypted); + if (value.extensionId === extensionId) { + return value.content; + } + } catch (_) { + throw new Error('Cannot get password'); + } + } + + return undefined; + } + + async $setPassword(extensionId: string, key: string, value: string): Promise { + const fullKey = this.getFullKey(extensionId); + const toEncrypt = JSON.stringify({ + extensionId, + content: value + }); + const encrypted = await this.encryptionService.encrypt(toEncrypt); + return this.credentialsService.setPassword(fullKey, key, encrypted); + } + + async $deletePassword(extensionId: string, key: string): Promise { + try { + const fullKey = this.getFullKey(extensionId); + await this.credentialsService.deletePassword(fullKey, key); + } catch (_) { + throw new Error('Cannot delete password'); + } + } +} diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index 2f4cb073c..3cc31f086 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -567,13 +567,19 @@ export class MainThreadTask implements MainThreadTaskShape { if (!task) { reject(new Error('Task not found')); } else { - this._taskService.run(task).then(undefined, reason => { - // eat the error, it has already been surfaced to the user and we don't care about it here - }); const result: TaskExecutionDTO = { id: value.id, task: TaskDTO.from(task) }; + this._taskService.run(task).then(summary => { + // Ensure that the task execution gets cleaned up if the exit code is undefined + // This can happen when the task has dependent tasks and one of them failed + if ((summary?.exitCode === undefined) || (summary.exitCode !== 0)) { + this._proxy.$OnDidEndTask(result); + } + }, reason => { + // eat the error, it has already been surfaced to the user and we don't care about it here + }); resolve(result); } }, (_error) => { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 8e54cb8f4..8dad46c6a 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { IShellLaunchConfig, ITerminalProcessExtHostProxy, ISpawnExtHostProcessRequest, ITerminalDimensions, EXT_HOST_CREATION_DELAY, IAvailableShellsRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, IShellLaunchConfigDto, TerminalLaunchConfig, ITerminalDimensionsDto } from 'vs/workbench/api/common/extHost.protocol'; +import { IShellLaunchConfig, ITerminalProcessExtHostProxy, ISpawnExtHostProcessRequest, ITerminalDimensions, IAvailableShellsRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest, ITerminalConfiguration, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, IShellLaunchConfigDto, TerminalLaunchConfig, ITerminalDimensionsDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { URI } from 'vs/base/common/uri'; import { StopWatch } from 'vs/base/common/stopwatch'; @@ -16,11 +16,18 @@ import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/termi import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; import { ILogService } from 'vs/platform/log/common/log'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) export class MainThreadTerminalService implements MainThreadTerminalServiceShape { private _proxy: ExtHostTerminalServiceShape; + /** + * Stores a map from a temporary terminal id (a UUID generated on the extension host side) + * to a numeric terminal id (an id generated on the renderer side) + * This comes in play only when dealing with terminals created on the extension host side + */ + private _extHostTerminalIds = new Map(); private _remoteAuthority: string | null; private readonly _toDispose = new DisposableStore(); private readonly _terminalProcessProxies = new Map(); @@ -40,6 +47,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService, + @IConfigurationService private readonly _configurationService: IConfigurationService, @ILogService private readonly _logService: ILogService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); @@ -47,13 +55,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape // ITerminalService listeners this._toDispose.add(_terminalService.onInstanceCreated((instance) => { - // Delay this message so the TerminalInstance constructor has a chance to finish and - // return the ID normally to the extension host. The ID that is passed here will be - // used to register non-extension API terminals in the extension host. - setTimeout(() => { - this._onTerminalOpened(instance); - this._onInstanceDimensionsChanged(instance); - }, EXT_HOST_CREATION_DELAY); + this._onTerminalOpened(instance); + this._onInstanceDimensionsChanged(instance); })); this._toDispose.add(_terminalService.onInstanceDisposed(instance => this._onTerminalDisposed(instance))); @@ -100,7 +103,22 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape // when the extension host process goes down ? } - public $createTerminal(launchConfig: TerminalLaunchConfig): Promise<{ id: number, name: string }> { + private _getTerminalId(id: TerminalIdentifier): number | undefined { + if (typeof id === 'number') { + return id; + } + return this._extHostTerminalIds.get(id); + } + + private _getTerminalInstance(id: TerminalIdentifier): ITerminalInstance | undefined { + const rendererId = this._getTerminalId(id); + if (typeof rendererId === 'number') { + return this._terminalService.getInstanceFromId(rendererId); + } + return undefined; + } + + public async $createTerminal(extHostTerminalId: string, launchConfig: TerminalLaunchConfig): Promise { const shellLaunchConfig: IShellLaunchConfig = { name: launchConfig.name, executable: launchConfig.shellPath, @@ -112,39 +130,38 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape strictEnv: launchConfig.strictEnv, hideFromUser: launchConfig.hideFromUser, isExtensionTerminal: launchConfig.isExtensionTerminal, + extHostTerminalId: extHostTerminalId, isFeatureTerminal: launchConfig.isFeatureTerminal }; const terminal = this._terminalService.createTerminal(shellLaunchConfig); - return Promise.resolve({ - id: terminal.id, - name: terminal.title - }); + this._extHostTerminalIds.set(extHostTerminalId, terminal.id); } - public $show(terminalId: number, preserveFocus: boolean): void { - const terminalInstance = this._terminalService.getInstanceFromId(terminalId); + public $show(id: TerminalIdentifier, preserveFocus: boolean): void { + const terminalInstance = this._getTerminalInstance(id); if (terminalInstance) { this._terminalService.setActiveInstance(terminalInstance); this._terminalService.showPanel(!preserveFocus); } } - public $hide(terminalId: number): void { + public $hide(id: TerminalIdentifier): void { + const rendererId = this._getTerminalId(id); const instance = this._terminalService.getActiveInstance(); - if (instance && instance.id === terminalId) { + if (instance && instance.id === rendererId) { this._terminalService.hidePanel(); } } - public $dispose(terminalId: number): void { - const terminalInstance = this._terminalService.getInstanceFromId(terminalId); + public $dispose(id: TerminalIdentifier): void { + const terminalInstance = this._getTerminalInstance(id); if (terminalInstance) { terminalInstance.dispose(); } } - public $sendText(terminalId: number, text: string, addNewLine: boolean): void { - const terminalInstance = this._terminalService.getInstanceFromId(terminalId); + public $sendText(id: TerminalIdentifier, text: string, addNewLine: boolean): void { + const terminalInstance = this._getTerminalInstance(id); if (terminalInstance) { terminalInstance.sendText(text, addNewLine); } @@ -204,6 +221,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } private _onTerminalOpened(terminalInstance: ITerminalInstance): void { + const extHostTerminalId = terminalInstance.shellLaunchConfig.extHostTerminalId; const shellLaunchConfigDto: IShellLaunchConfigDto = { name: terminalInstance.shellLaunchConfig.name, executable: terminalInstance.shellLaunchConfig.executable, @@ -212,13 +230,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape env: terminalInstance.shellLaunchConfig.env, hideFromUser: terminalInstance.shellLaunchConfig.hideFromUser }; - if (terminalInstance.title) { - this._proxy.$acceptTerminalOpened(terminalInstance.id, terminalInstance.title, shellLaunchConfigDto); - } else { - terminalInstance.waitForTitle().then(title => { - this._proxy.$acceptTerminalOpened(terminalInstance.id, title, shellLaunchConfigDto); - }); - } + this._proxy.$acceptTerminalOpened(terminalInstance.id, extHostTerminalId, terminalInstance.title, shellLaunchConfigDto); } private _onTerminalProcessIdReady(terminalInstance: ITerminalInstance): void { @@ -249,7 +261,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape executable: request.shellLaunchConfig.executable, args: request.shellLaunchConfig.args, cwd: request.shellLaunchConfig.cwd, - env: request.shellLaunchConfig.env + env: request.shellLaunchConfig.env, + flowControl: this._configurationService.getValue(TERMINAL_CONFIG_SECTION).flowControl }; this._logService.trace('Spawning ext host process', { terminalId: proxy.terminalId, shellLaunchConfigDto, request }); @@ -260,8 +273,9 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape request.cols, request.rows, request.isWorkspaceShellAllowed - ).then(request.callback); + ).then(request.callback, request.callback); + proxy.onAcknowledgeDataEvent(charCount => this._proxy.$acceptProcessAckDataEvent(proxy.terminalId, charCount)); proxy.onInput(data => this._proxy.$acceptProcessInput(proxy.terminalId, data)); proxy.onResize(dimensions => this._proxy.$acceptProcessResize(proxy.terminalId, dimensions.cols, dimensions.rows)); proxy.onShutdown(immediate => this._proxy.$acceptProcessShutdown(proxy.terminalId, immediate)); @@ -294,38 +308,59 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } public $sendProcessTitle(terminalId: number, title: string): void { - this._getTerminalProcess(terminalId).emitTitle(title); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitTitle(title); + } } public $sendProcessData(terminalId: number, data: string): void { - this._getTerminalProcess(terminalId).emitData(data); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitData(data); + } } public $sendProcessReady(terminalId: number, pid: number, cwd: string): void { - this._getTerminalProcess(terminalId).emitReady(pid, cwd); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitReady(pid, cwd); + } } public $sendProcessExit(terminalId: number, exitCode: number | undefined): void { - this._getTerminalProcess(terminalId).emitExit(exitCode); - this._terminalProcessProxies.delete(terminalId); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitExit(exitCode); + this._terminalProcessProxies.delete(terminalId); + } } public $sendOverrideDimensions(terminalId: number, dimensions: ITerminalDimensions | undefined): void { - this._getTerminalProcess(terminalId).emitOverrideDimensions(dimensions); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitOverrideDimensions(dimensions); + } } public $sendProcessInitialCwd(terminalId: number, initialCwd: string): void { - this._getTerminalProcess(terminalId).emitInitialCwd(initialCwd); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitInitialCwd(initialCwd); + } } public $sendProcessCwd(terminalId: number, cwd: string): void { - this._getTerminalProcess(terminalId).emitCwd(cwd); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitCwd(cwd); + } } public $sendResolvedLaunchConfig(terminalId: number, shellLaunchConfig: IShellLaunchConfig): void { const instance = this._terminalService.getInstanceFromId(terminalId); if (instance) { - this._getTerminalProcess(terminalId).emitResolvedShellLaunchConfig(shellLaunchConfig); + this._getTerminalProcess(terminalId)?.emitResolvedShellLaunchConfig(shellLaunchConfig); } } @@ -338,7 +373,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape sw.stop(); sum += sw.elapsed(); } - this._getTerminalProcess(terminalId).emitLatency(sum / COUNT); + this._getTerminalProcess(terminalId)?.emitLatency(sum / COUNT); } private _isPrimaryExtHost(): boolean { @@ -363,10 +398,11 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } } - private _getTerminalProcess(terminalId: number): ITerminalProcessExtHostProxy { + private _getTerminalProcess(terminalId: number): ITerminalProcessExtHostProxy | undefined { const terminal = this._terminalProcessProxies.get(terminalId); if (!terminal) { - throw new Error(`Unknown terminal: ${terminalId}`); + this._logService.error(`Unknown terminal: ${terminalId}`); + return undefined; } return terminal; } diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index 52185f8dd..370f08df9 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -3,12 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { getTestSubscriptionKey, RunTestsRequest, RunTestsResult, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; -import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { getTestSubscriptionKey, RunTestsRequest, RunTestsResult, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; + +const reviveDiff = (diff: TestsDiff) => { + for (const entry of diff) { + if (entry[0] === TestDiffOpType.Add || entry[0] === TestDiffOpType.Update) { + const item = entry[1]; + if (item.item.location) { + item.item.location.uri = URI.revive(item.item.location.uri); + } + + for (const message of item.item.state.messages) { + if (message.location) { + message.location.uri = URI.revive(message.location.uri); + } + } + } + } +}; @extHostNamedCustomer(MainContext.MainThreadTesting) export class MainThreadTesting extends Disposable implements MainThreadTestingShape { @@ -18,11 +37,28 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh constructor( extHostContext: IExtHostContext, @ITestService private readonly testService: ITestService, + @ITestResultService resultService: ITestResultService, ) { super(); this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostTesting); this._register(this.testService.onShouldSubscribe(args => this.proxy.$subscribeToTests(args.resource, args.uri))); this._register(this.testService.onShouldUnsubscribe(args => this.proxy.$unsubscribeFromTests(args.resource, args.uri))); + + const testCompleteListener = this._register(new MutableDisposable()); + this._register(resultService.onNewTestResult(results => { + testCompleteListener.value = results.onComplete(() => this.proxy.$publishTestResults({ tests: results.tests })); + })); + + testService.updateRootProviderCount(1); + + const lastCompleted = resultService.results.find(r => !r.isComplete); + if (lastCompleted) { + this.proxy.$publishTestResults({ tests: lastCompleted.tests }); + } + + for (const { resource, uri } of this.testService.subscriptions) { + this.proxy.$subscribeToTests(resource, uri); + } } /** @@ -30,7 +66,8 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh */ public $registerTestProvider(id: string) { this.testService.registerTestController(id, { - runTests: req => this.proxy.$runTestsForProvider(req), + runTests: (req, token) => this.proxy.$runTestsForProvider(req, token), + lookupTest: test => this.proxy.$lookupTest(test), }); } @@ -64,14 +101,19 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh * @inheritdoc */ public $publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void { + reviveDiff(diff); this.testService.publishDiff(resource, URI.revive(uri), diff); } - public $runTests(req: RunTestsRequest): Promise { - return this.testService.runTests(req); + public $runTests(req: RunTestsRequest, token: CancellationToken): Promise { + return this.testService.runTests(req, token); } public dispose() { - // no-op + this.testService.updateRootProviderCount(-1); + for (const subscription of this.testSubscriptions.values()) { + subscription.dispose(); + } + this.testSubscriptions.clear(); } } diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 8e32acd7f..cfe9d842e 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -222,8 +222,8 @@ class TreeViewDataProvider implements ITreeViewDataProvider { const hasResolve = await this.hasResolve; if (elements) { for (const element of elements) { - const resolvable = new ResolvableTreeItem(element, hasResolve ? () => { - return this._proxy.$resolve(this.treeViewId, element.handle); + const resolvable = new ResolvableTreeItem(element, hasResolve ? (token) => { + return this._proxy.$resolve(this.treeViewId, element.handle, token); } : undefined); this.itemsMap.set(element.handle, resolvable); result.push(resolvable); diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index ac9b83562..82425aadb 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -3,22 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import { MainThreadTunnelServiceShape, IExtHostContext, MainContext, ExtHostContext, ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { IRemoteExplorerService, makeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService'; -import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel'; +import { CandidatePort, IRemoteExplorerService, makeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, RemoteTunnel, isPortPrivileged } from 'vs/platform/remote/common/tunnel'; import { Disposable } from 'vs/base/common/lifecycle'; import type { TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { PORT_AUTO_FORWARD_SETTING } from 'vs/workbench/contrib/remote/browser/tunnelView'; +import { ILogService } from 'vs/platform/log/common/log'; @extHostNamedCustomer(MainContext.MainThreadTunnelService) export class MainThreadTunnelService extends Disposable implements MainThreadTunnelServiceShape { private readonly _proxy: ExtHostTunnelServiceShape; + private elevateionRetry: boolean = false; constructor( extHostContext: IExtHostContext, @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, - @ITunnelService private readonly tunnelService: ITunnelService + @ITunnelService private readonly tunnelService: ITunnelService, + @INotificationService private readonly notificationService: INotificationService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ILogService private readonly logService: ILogService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTunnelService); @@ -26,14 +35,50 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun this._register(tunnelService.onTunnelClosed(() => this._proxy.$onDidTunnelsChange())); } + async $setCandidateFinder(): Promise { + if (this.remoteExplorerService.portsFeaturesEnabled) { + this._proxy.$registerCandidateFinder(this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)); + } else { + this._register(this.remoteExplorerService.onEnabledPortsFeatures(() => this._proxy.$registerCandidateFinder(this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)))); + } + this._register(this.configurationService.onDidChangeConfiguration(async (e) => { + if (e.affectsConfiguration(PORT_AUTO_FORWARD_SETTING)) { + return this._proxy.$registerCandidateFinder((this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING))); + } + })); + } + async $openTunnel(tunnelOptions: TunnelOptions, source: string): Promise { - const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label, source); + const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label, source, false); if (tunnel) { + if (!this.elevateionRetry + && (tunnelOptions.localAddressPort !== undefined) + && (tunnel.tunnelLocalPort !== undefined) + && isPortPrivileged(tunnelOptions.localAddressPort) + && (tunnel.tunnelLocalPort !== tunnelOptions.localAddressPort) + && this.tunnelService.canElevate) { + + this.elevationPrompt(tunnelOptions, tunnel, source); + } return TunnelDto.fromServiceTunnel(tunnel); } return undefined; } + private async elevationPrompt(tunnelOptions: TunnelOptions, tunnel: RemoteTunnel, source: string) { + return this.notificationService.prompt(Severity.Info, + nls.localize('remote.tunnel.openTunnel', "The extension {0} has forwarded port {1}. You'll need to run as superuser to use port {2} locally.", source, tunnelOptions.remoteAddress.port, tunnelOptions.localAddressPort), + [{ + label: nls.localize('remote.tunnelsView.elevationButton', "Use Port {0} as Sudo...", tunnel.tunnelRemotePort), + run: async () => { + this.elevateionRetry = true; + await this.remoteExplorerService.close({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); + await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label, source, true); + this.elevateionRetry = false; + } + }]); + } + async $closeTunnel(remote: { host: string, port: number }): Promise { return this.remoteExplorerService.close(remote); } @@ -47,27 +92,29 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun }); } - async $onFoundNewCandidates(candidates: { host: string, port: number, detail: string }[]): Promise { + async $onFoundNewCandidates(candidates: CandidatePort[]): Promise { this.remoteExplorerService.onFoundNewCandidates(candidates); } - async $tunnelServiceReady(): Promise { - return this.remoteExplorerService.restore(); - } - - async $setTunnelProvider(): Promise { + async $setTunnelProvider(features: TunnelProviderFeatures): Promise { const tunnelProvider: ITunnelProvider = { forwardPort: (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions) => { const forward = this._proxy.$forwardPort(tunnelOptions, tunnelCreationOptions); if (forward) { return forward.then(tunnel => { + this.logService.trace(`MainThreadTunnelService: New tunnel established by tunnel provider: ${tunnel?.remoteAddress.host}:${tunnel?.remoteAddress.port}`); + if (!tunnel) { + return undefined; + } return { tunnelRemotePort: tunnel.remoteAddress.port, tunnelRemoteHost: tunnel.remoteAddress.host, localAddress: typeof tunnel.localAddress === 'string' ? tunnel.localAddress : makeAddress(tunnel.localAddress.host, tunnel.localAddress.port), tunnelLocalPort: typeof tunnel.localAddress !== 'string' ? tunnel.localAddress.port : undefined, - dispose: (silent?: boolean) => { - this._proxy.$closeTunnel({ host: tunnel.remoteAddress.host, port: tunnel.remoteAddress.port }, silent); + public: tunnel.public, + dispose: async (silent?: boolean) => { + this.logService.trace(`MainThreadTunnelService: Closing tunnel from tunnel provider: ${tunnel?.remoteAddress.host}:${tunnel?.remoteAddress.port}`); + return this._proxy.$closeTunnel({ host: tunnel.remoteAddress.host, port: tunnel.remoteAddress.port }, silent); } }; }); @@ -75,9 +122,16 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun return undefined; } }; - this.tunnelService.setTunnelProvider(tunnelProvider); + this.tunnelService.setTunnelProvider(tunnelProvider, features); } + async $setCandidateFilter(): Promise { + this.remoteExplorerService.setCandidateFilter((candidates: CandidatePort[]): Promise => { + return this._proxy.$applyCandidateFilter(candidates); + }); + } + + dispose(): void { } diff --git a/src/vs/workbench/api/browser/mainThreadUriOpeners.ts b/src/vs/workbench/api/browser/mainThreadUriOpeners.ts new file mode 100644 index 000000000..9ac5d76c1 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadUriOpeners.ts @@ -0,0 +1,132 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action } from 'vs/base/common/actions'; +import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ExtHostContext, ExtHostUriOpenersShape, IExtHostContext, MainContext, MainThreadUriOpenersShape } from 'vs/workbench/api/common/extHost.protocol'; +import { defaultExternalUriOpenerId } from 'vs/workbench/contrib/externalUriOpener/common/configuration'; +import { ContributedExternalUriOpenersStore } from 'vs/workbench/contrib/externalUriOpener/common/contributedOpeners'; +import { IExternalOpenerProvider, IExternalUriOpener, IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { extHostNamedCustomer } from '../common/extHostCustomers'; + +interface RegisteredOpenerMetadata { + readonly schemes: ReadonlySet; + readonly extensionId: ExtensionIdentifier; + readonly label: string; +} + +@extHostNamedCustomer(MainContext.MainThreadUriOpeners) +export class MainThreadUriOpeners extends Disposable implements MainThreadUriOpenersShape, IExternalOpenerProvider { + + private readonly proxy: ExtHostUriOpenersShape; + private readonly _registeredOpeners = new Map(); + private readonly _contributedExternalUriOpenersStore: ContributedExternalUriOpenersStore; + + constructor( + context: IExtHostContext, + @IStorageService storageService: IStorageService, + @IExternalUriOpenerService externalUriOpenerService: IExternalUriOpenerService, + @IExtensionService private readonly extensionService: IExtensionService, + @IOpenerService private readonly openerService: IOpenerService, + @INotificationService private readonly notificationService: INotificationService, + ) { + super(); + this.proxy = context.getProxy(ExtHostContext.ExtHostUriOpeners); + + this._register(externalUriOpenerService.registerExternalOpenerProvider(this)); + + this._contributedExternalUriOpenersStore = this._register(new ContributedExternalUriOpenersStore(storageService, extensionService)); + } + + public async *getOpeners(targetUri: URI): AsyncIterable { + + // Currently we only allow openers for http and https urls + if (targetUri.scheme !== Schemas.http && targetUri.scheme !== Schemas.https) { + return; + } + + await this.extensionService.activateByEvent(`onUriOpen:${targetUri.scheme}`); + + for (const [id, openerMetadata] of this._registeredOpeners) { + if (openerMetadata.schemes.has(targetUri.scheme)) { + yield this.createOpener(id, openerMetadata); + } + } + } + + private createOpener(id: string, metadata: RegisteredOpenerMetadata): IExternalUriOpener { + return { + id: id, + label: metadata.label, + canOpen: (uri, token) => { + return this.proxy.$canOpenUri(id, uri, token); + }, + openExternalUri: async (uri, ctx, token) => { + try { + await this.proxy.$openUri(id, { resolvedUri: uri, sourceUri: ctx.sourceUri }, token); + } catch (e) { + if (!isPromiseCanceledError(e)) { + const openDefaultAction = new Action('default', localize('openerFailedUseDefault', "Open using default opener"), undefined, undefined, () => { + return this.openerService.open(uri, { + allowTunneling: false, + allowContributedOpeners: defaultExternalUriOpenerId, + }); + }); + openDefaultAction.tooltip = uri.toString(); + + this.notificationService.notify({ + severity: Severity.Error, + message: localize('openerFailedMessage', 'Could not open uri with \'{0}\': {1}', id, e.toString()), + actions: { + primary: [ + openDefaultAction + ] + } + }); + } + } + return true; + }, + }; + } + + async $registerUriOpener( + id: string, + schemes: readonly string[], + extensionId: ExtensionIdentifier, + label: string, + ): Promise { + if (this._registeredOpeners.has(id)) { + throw new Error(`Opener with id '${id}' already registered`); + } + + this._registeredOpeners.set(id, { + schemes: new Set(schemes), + label, + extensionId, + }); + + this._contributedExternalUriOpenersStore.add(id, extensionId.value); + } + + async $unregisterUriOpener(id: string): Promise { + this._registeredOpeners.delete(id); + this._contributedExternalUriOpenersStore.delete(id); + } + + dispose(): void { + super.dispose(); + this._registeredOpeners.clear(); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts index 6428e6f48..7aa690772 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts @@ -129,6 +129,9 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc dispose(this._editorProviders.values()); this._editorProviders.clear(); + + dispose(this._revivers.values()); + this._revivers.clear(); } public get webviewInputs(): Iterable { return this._webviewInputs; } @@ -181,7 +184,6 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc webview.setName(value); } - public $setIconPath(handle: extHostProtocol.WebviewHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void { const webview = this.getWebviewInput(handle); webview.iconPath = reviveWebviewIcon(value); @@ -199,8 +201,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc } } - public $registerSerializer(viewType: string) - : void { + public $registerSerializer(viewType: string): void { if (this._revivers.has(viewType)) { throw new Error(`Reviver for ${viewType} already registered`); } @@ -216,7 +217,6 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc return; } - const handle = webviewInput.id; this.addWebviewInput(handle, webviewInput); diff --git a/src/vs/workbench/api/browser/mainThreadWebviews.ts b/src/vs/workbench/api/browser/mainThreadWebviews.ts index cc5b05c29..684096227 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviews.ts @@ -78,7 +78,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma private onDidClickLink(handle: extHostProtocol.WebviewHandle, link: string): void { const webview = this.getWebview(handle); if (this.isSupportedLink(webview, URI.parse(link))) { - this._openerService.open(link, { fromUserGesture: true }); + this._openerService.open(link, { fromUserGesture: true, allowContributedOpeners: true }); } } diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index 2a431111e..7f10aa334 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -52,7 +52,11 @@ export class MainThreadWindow implements MainThreadWindowShape { // called with URI or transformed -> use uri target = uri; } - return this.openerService.open(target, { openExternal: true, allowTunneling: options.allowTunneling }); + return this.openerService.open(target, { + openExternal: true, + allowTunneling: options.allowTunneling, + allowContributedOpeners: options.allowContributedOpeners, + }); } async $asExternalUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 252fa8e42..946c7bd3d 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -6,25 +6,25 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { isNative } from 'vs/base/common/platform'; +import { withNullAsUndefined } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { isNative } from 'vs/base/common/platform'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'vs/workbench/services/search/common/search'; -import { IWorkspaceContextService, WorkbenchState, IWorkspace, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { IWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { checkGlobFileExists } from 'vs/workbench/api/common/shared/workspaceContains'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'vs/workbench/services/search/common/search'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; -import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, MainContext, MainThreadWorkspaceShape, IWorkspaceData, ITextSearchComplete } from '../common/extHost.protocol'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { withNullAsUndefined } from 'vs/base/common/types'; -import { IFileService } from 'vs/platform/files/common/files'; -import { IRequestService } from 'vs/platform/request/common/request'; -import { checkGlobFileExists } from 'vs/workbench/api/common/shared/workspaceContains'; +import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, ITextSearchComplete, IWorkspaceData, MainContext, MainThreadWorkspaceShape } from '../common/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { @@ -139,7 +139,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { } const query = this._queryBuilder.file( - includeFolder ? [toWorkspaceFolder(includeFolder)] : workspace.folders, + includeFolder ? [includeFolder] : workspace.folders, { maxResults: withNullAsUndefined(maxResults), disregardExcludeSettings: (excludePatternOrDisregardExcludes === false) || undefined, diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 6be78f4ec..84b95cfc7 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -3,35 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; +import { coalesce } from 'vs/base/common/arrays'; import { forEach } from 'vs/base/common/collections'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as resources from 'vs/base/common/resources'; -import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { ViewContainer, IViewsRegistry, ITreeViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, TEST_VIEW_CONTAINER_ID, IViewDescriptor, ViewContainerLocation, testViewIcon } from 'vs/workbench/common/views'; -import { CustomTreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { coalesce, } from 'vs/base/common/arrays'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files'; -import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm'; -import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug'; -import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; -import { ViewletRegistry, Extensions as ViewletExtensions, ShowViewletAction } from 'vs/workbench/browser/viewlet'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { localize } from 'vs/nls'; +import { registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { CustomTreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { Extensions as ViewletExtensions, ShowViewletAction2, ViewletRegistry } from 'vs/workbench/browser/viewlet'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { Extensions as ViewContainerExtensions, ITreeViewDescriptor, IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; +import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug'; +import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files'; +import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer'; +import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm'; +import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane'; +import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; export interface IUserFriendlyViewsContainerDescriptor { id: string; @@ -110,7 +107,7 @@ const viewDescriptor: IJSONSchema = { defaultSnippets: [{ body: { id: '${1:id}', name: '${2:name}' } }], properties: { type: { - markdownDescription: localize('vscode.extension.contributes.view.type', "Type of the the view. This can either be `tree` for a tree view based view or `webview` for a webview based view. The default is `tree`."), + markdownDescription: localize('vscode.extension.contributes.view.type', "Type of the view. This can either be `tree` for a tree view based view or `webview` for a webview based view. The default is `tree`."), type: 'string', enum: [ 'tree', @@ -255,7 +252,8 @@ const viewsExtensionPoint: IExtensionPoint = ExtensionsR jsonSchema: viewsContribution }); -const TEST_VIEW_CONTAINER_ORDER = 6; +const CUSTOM_VIEWS_START_ORDER = 7; + class ViewsExtensionHandler implements IWorkbenchContribution { private viewContainersRegistry: IViewContainersRegistry; @@ -271,7 +269,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution { } private handleAndRegisterCustomViewContainers() { - this.registerTestViewContainer(); viewsContainersExtensionPoint.setHandler((extensions, { added, removed }) => { if (removed.length) { this.removeCustomViewContainers(removed); @@ -284,7 +281,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { private addCustomViewContainers(extensionPoints: readonly IExtensionPointUser[], existingViewContainers: ViewContainer[]): void { const viewContainersRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); - let activityBarOrder = TEST_VIEW_CONTAINER_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Sidebar).length + 1; + let activityBarOrder = CUSTOM_VIEWS_START_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Sidebar).length; let panelOrder = 5 + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Panel).length + 1; for (let { value, collector, description } of extensionPoints) { forEach(value, entry => { @@ -318,13 +315,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution { } } - private registerTestViewContainer(): void { - const title = localize('test', "Test"); - const icon = testViewIcon; - - this.registerCustomViewContainer(TEST_VIEW_CONTAINER_ID, title, icon, TEST_VIEW_CONTAINER_ORDER, undefined, ViewContainerLocation.Sidebar); - } - private isValidViewsContainer(viewsContainersDescriptors: IUserFriendlyViewsContainerDescriptor[], collector: ExtensionMessageCollector): boolean { if (!Array.isArray(viewsContainersDescriptors)) { collector.error(localize('viewcontainer requirearray', "views containers must be an array")); @@ -395,22 +385,15 @@ class ViewsExtensionHandler implements IWorkbenchContribution { }, location); // Register Action to Open Viewlet - class OpenCustomViewletAction extends ShowViewletAction { - constructor( - id: string, label: string, - @IViewletService viewletService: IViewletService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, id, viewletService, editorGroupService, layoutService); + registerAction2(class OpenCustomViewletAction extends ShowViewletAction2 { + constructor() { + super({ id, f1: true, title: localize('showViewlet', "Show {0}", title), category: CATEGORIES.View.value }); } - } - const registry = Registry.as(ActionExtensions.WorkbenchActions); - registry.registerWorkbenchAction( - SyncActionDescriptor.create(OpenCustomViewletAction, id, localize('showViewlet', "Show {0}", title)), - `View: Show ${title}`, - CATEGORIES.View.value - ); + + protected viewletId() { + return id; + } + }); } return viewContainer; @@ -501,7 +484,8 @@ class ViewsExtensionHandler implements IWorkbenchContribution { originalContainerId: entry.key, group: item.group, remoteAuthority: item.remoteName || (item).remoteAuthority, // TODO@roblou - delete after remote extensions are updated - hideByDefault: initialVisibility === InitialVisibility.Hidden + hideByDefault: initialVisibility === InitialVisibility.Hidden, + workspace: viewContainer?.id === REMOTE ? true : undefined }; diff --git a/src/vs/workbench/api/common/apiCommands.ts b/src/vs/workbench/api/common/apiCommands.ts index 7670549c6..d4a7ea407 100644 --- a/src/vs/workbench/api/common/apiCommands.ts +++ b/src/vs/workbench/api/common/apiCommands.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI, UriComponents } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { CommandsRegistry, ICommandService, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -12,7 +12,6 @@ import { IWorkspacesService, IRecent } from 'vs/platform/workspaces/common/works import { ILogService } from 'vs/platform/log/common/log'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IViewDescriptorService, IViewsService, ViewVisibilityState } from 'vs/workbench/common/views'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; // ----------------------------------------------------------------- // The following commands are registered on both sides separately. @@ -128,12 +127,6 @@ CommandsRegistry.registerCommand('_extensionTests.setLogLevel', function (access } }); -CommandsRegistry.registerCommand('_workbench.openExternal', function (accessor: ServicesAccessor, uri: UriComponents, options: { allowTunneling?: boolean }) { - // TODO: discuss martin, ben where to put this - const openerService = accessor.get(IOpenerService); - openerService.open(URI.revive(uri), options); -}); - CommandsRegistry.registerCommand('_extensionTests.getLogLevel', function (accessor: ServicesAccessor) { const logService = accessor.get(ILogService); diff --git a/src/vs/workbench/api/common/exHostSecretState.ts b/src/vs/workbench/api/common/exHostSecretState.ts new file mode 100644 index 000000000..b06f12450 --- /dev/null +++ b/src/vs/workbench/api/common/exHostSecretState.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtHostSecretStateShape, MainContext, MainThreadSecretStateShape } from 'vs/workbench/api/common/extHost.protocol'; +import { Emitter } from 'vs/base/common/event'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export class ExtHostSecretState implements ExtHostSecretStateShape { + private _proxy: MainThreadSecretStateShape; + private _onDidChangePassword = new Emitter<{ extensionId: string, key: string }>(); + readonly onDidChangePassword = this._onDidChangePassword.event; + + constructor(mainContext: IExtHostRpcService) { + this._proxy = mainContext.getProxy(MainContext.MainThreadSecretState); + } + + async $onDidChangePassword(e: { extensionId: string, key: string }): Promise { + this._onDidChangePassword.fire(e); + } + + get(extensionId: string, key: string): Promise { + return this._proxy.$getPassword(extensionId, key); + } + + store(extensionId: string, key: string, value: string): Promise { + return this._proxy.$setPassword(extensionId, key, value); + } + + delete(extensionId: string, key: string): Promise { + return this._proxy.$deletePassword(extensionId, key); + } +} + +export interface IExtHostSecretState extends ExtHostSecretState { } +export const IExtHostSecretState = createDecorator('IExtHostSecretState'); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index fa880fcfc..2da02381a 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -82,6 +82,9 @@ import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPane import { ExtHostBulkEdits } from 'vs/workbench/api/common/extHostBulkEdits'; import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; import { ExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; +import { ExtHostUriOpeners } from 'vs/workbench/api/common/extHostUriOpener'; +import { IExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; +import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -107,6 +110,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostTunnelService = accessor.get(IExtHostTunnelService); const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService); const extHostWindow = accessor.get(IExtHostWindow); + const extHostSecretState = accessor.get(IExtHostSecretState); // register addressable instances rpcProtocol.set(ExtHostContext.ExtHostFileSystemInfo, extHostFileSystemInfo); @@ -117,6 +121,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage); rpcProtocol.set(ExtHostContext.ExtHostTunnelService, extHostTunnelService); rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow); + rpcProtocol.set(ExtHostContext.ExtHostSecretState, extHostSecretState); // automatically create and register addressable instances const extHostDecorations = rpcProtocol.set(ExtHostContext.ExtHostDecorations, accessor.get(IExtHostDecorations)); @@ -129,6 +134,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostOutputService = rpcProtocol.set(ExtHostContext.ExtHostOutputService, accessor.get(IExtHostOutputService)); // manually create and register addressable instances + const extHostEditorTabs = rpcProtocol.set(ExtHostContext.ExtHostEditorTabs, new ExtHostEditorTabs()); const extHostUrls = rpcProtocol.set(ExtHostContext.ExtHostUrls, new ExtHostUrls(rpcProtocol)); const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); @@ -154,6 +160,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, new ExtHostTesting(rpcProtocol, extHostDocumentsAndEditors, extHostWorkspace)); + const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol)); // Check that no named customers are missing const expected: ProxyIdentifier[] = values(ExtHostContext); @@ -209,22 +216,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get onDidChangeSessions(): Event { return extHostAuthentication.onDidChangeSessions; }, - registerAuthenticationProvider(provider: vscode.AuthenticationProvider): vscode.Disposable { + registerAuthenticationProvider(id: string, label: string, provider: vscode.AuthenticationProvider, options?: vscode.AuthenticationProviderOptions): vscode.Disposable { checkProposedApiEnabled(extension); - return extHostAuthentication.registerAuthenticationProvider(provider); + return extHostAuthentication.registerAuthenticationProvider(id, label, provider, options); }, get onDidChangeAuthenticationProviders(): Event { checkProposedApiEnabled(extension); return extHostAuthentication.onDidChangeAuthenticationProviders; }, - getProviderIds(): Thenable> { - checkProposedApiEnabled(extension); - return extHostAuthentication.getProviderIds(); - }, - get providerIds(): string[] { - checkProposedApiEnabled(extension); - return extHostAuthentication.providerIds; - }, get providers(): ReadonlyArray { checkProposedApiEnabled(extension); return extHostAuthentication.providers; @@ -232,22 +231,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I logout(providerId: string, sessionId: string): Thenable { checkProposedApiEnabled(extension); return extHostAuthentication.logout(providerId, sessionId); - }, - getPassword(key: string): Thenable { - checkProposedApiEnabled(extension); - return extHostAuthentication.getPassword(extension, key); - }, - setPassword(key: string, value: string): Thenable { - checkProposedApiEnabled(extension); - return extHostAuthentication.setPassword(extension, key, value); - }, - deletePassword(key: string): Thenable { - checkProposedApiEnabled(extension); - return extHostAuthentication.deletePassword(extension, key); - }, - get onDidChangePassword(): Event { - checkProposedApiEnabled(extension); - return extHostAuthentication.onDidChangePassword; } }; @@ -311,8 +294,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get shell() { return extHostTerminalService.getDefaultShell(false, configProvider); }, - openExternal(uri: URI) { - return extHostWindow.openUri(uri, { allowTunneling: !!initData.remote.authority }); + openExternal(uri: URI, options?: { allowContributedOpeners?: boolean | string; }) { + return extHostWindow.openUri(uri, { + allowTunneling: !!initData.remote.authority, + allowContributedOpeners: options?.allowContributedOpeners, + }); }, asExternalUri(uri: URI) { if (uri.scheme === initData.environment.appUriScheme) { @@ -354,6 +340,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostTesting.runTests(provider); }, + get onDidChangeTestResults() { + checkProposedApiEnabled(extension); + return extHostTesting.onLastResultsChanged; + }, + get testResults() { + checkProposedApiEnabled(extension); + return extHostTesting.lastResults; + }, }; // namespace: extensions @@ -480,6 +474,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I getTokenInformationAtPosition(doc: vscode.TextDocument, pos: vscode.Position) { checkProposedApiEnabled(extension); return extHostLanguages.tokenAtPosition(doc, pos); + }, + registerInlineHintsProvider(selector: vscode.DocumentSelector, provider: vscode.InlineHintsProvider): vscode.Disposable { + checkProposedApiEnabled(extension); + return extHostLanguageFeatures.registerInlineHintsProvider(extension, selector, provider); } }; @@ -588,10 +586,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I id = extension.identifier.value; name = nls.localize('extensionLabel', "{0} (Extension)", extension.displayName || extension.name); alignment = alignmentOrOptions; - priority = priority; } - return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority, accessibilityInformation, extension); + return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority, accessibilityInformation); }, setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): vscode.Disposable { return extHostStatusBar.setStatusBarMessage(text, timeoutOrThenable); @@ -691,6 +688,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I showNotebookDocument(document, options?) { checkProposedApiEnabled(extension); return extHostNotebook.showNotebookDocument(document, options); + }, + registerExternalUriOpener(id: string, opener: vscode.ExternalUriOpener, metadata: vscode.ExternalUriOpenerMetadata) { + checkProposedApiEnabled(extension); + return extHostUriOpeners.registerExternalUriOpener(extension.identifier, id, opener, metadata); + }, + get openEditors() { + checkProposedApiEnabled(extension); + return extHostEditorTabs.tabs; + }, + get onDidChangeOpenEditors() { + checkProposedApiEnabled(extension); + return extHostEditorTabs.onDidChangeTabs; } }; @@ -1108,6 +1117,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CallHierarchyIncomingCall: extHostTypes.CallHierarchyIncomingCall, CallHierarchyItem: extHostTypes.CallHierarchyItem, CallHierarchyOutgoingCall: extHostTypes.CallHierarchyOutgoingCall, + CancellationError: errors.CancellationError, CancellationTokenSource: CancellationTokenSource, CodeAction: extHostTypes.CodeAction, CodeActionKind: extHostTypes.CodeActionKind, @@ -1148,6 +1158,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I EventEmitter: Emitter, ExtensionKind: extHostTypes.ExtensionKind, ExtensionMode: extHostTypes.ExtensionMode, + ExternalUriOpenerPriority: extHostTypes.ExternalUriOpenerPriority, FileChangeType: extHostTypes.FileChangeType, FileDecoration: extHostTypes.FileDecoration, FileSystemError: extHostTypes.FileSystemError, @@ -1157,6 +1168,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I FunctionBreakpoint: extHostTypes.FunctionBreakpoint, Hover: extHostTypes.Hover, IndentAction: languageConfiguration.IndentAction, + InlineHint: extHostTypes.InlineHint, Location: extHostTypes.Location, MarkdownString: extHostTypes.MarkdownString, OverviewRulerLane: OverviewRulerLane, diff --git a/src/vs/workbench/api/common/extHost.common.services.ts b/src/vs/workbench/api/common/extHost.common.services.ts index 6c1a02f44..b1fe830b2 100644 --- a/src/vs/workbench/api/common/extHost.common.services.ts +++ b/src/vs/workbench/api/common/extHost.common.services.ts @@ -21,6 +21,7 @@ import { IExtHostApiDeprecationService, ExtHostApiDeprecationService, } from 'vs import { IExtHostWindow, ExtHostWindow } from 'vs/workbench/api/common/extHostWindow'; import { IExtHostConsumerFileSystem, ExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { IExtHostFileSystemInfo, ExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; +import { IExtHostSecretState, ExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths); registerSingleton(IExtHostApiDeprecationService, ExtHostApiDeprecationService); @@ -39,3 +40,4 @@ registerSingleton(IExtHostTerminalService, WorkerExtHostTerminalService); registerSingleton(IExtHostTunnelService, ExtHostTunnelService); registerSingleton(IExtHostWindow, ExtHostWindow); registerSingleton(IExtHostWorkspace, ExtHostWorkspace); +registerSingleton(IExtHostSecretState, ExtHostSecretState); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index b97a962f6..be7a14f88 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as performance from 'vs/base/common/performance'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IRemoteConsoleLog } from 'vs/base/common/console'; @@ -41,13 +42,13 @@ import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views'; import { IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; import { ITerminalDimensions, IShellLaunchConfig, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ActivationKind, ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationKind, ExtensionActivationError, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import * as search from 'vs/workbench/services/search/common/search'; import { EditorGroupColumn, SaveReason } from 'vs/workbench/common/editor'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; -import { TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel'; +import { TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEventDto, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2, TransientMetadata, INotebookCellStatusBarEntry, ICellRange, INotebookDecorationRenderOptions, INotebookExclusiveDocumentFilter } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -57,7 +58,8 @@ import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; -import { RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, InternalTestResults, RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -108,7 +110,8 @@ export interface IConfigurationInitData extends IConfigurationData { } export interface IExtHostContext extends IRPCProtocol { - remoteAuthority: string | null; + readonly remoteAuthority: string | null; + readonly extensionHostKind: ExtensionHostKind; } export interface IMainContext extends IRPCProtocol { @@ -174,7 +177,9 @@ export interface MainThreadAuthenticationShape extends IDisposable { $getSessions(providerId: string): Promise>; $login(providerId: string, scopes: string[]): Promise; $logout(providerId: string, sessionId: string): Promise; +} +export interface MainThreadSecretStateShape extends IDisposable { $getPassword(extensionId: string, key: string): Promise; $setPassword(extensionId: string, key: string, value: string): Promise; $deletePassword(extensionId: string, key: string): Promise; @@ -330,7 +335,7 @@ export interface IIndentationRuleDto { export interface IOnEnterRuleDto { beforeText: IRegExpDto; afterText?: IRegExpDto; - oneLineAboveText?: IRegExpDto; + previousLineText?: IRegExpDto; action: EnterAction; } export interface ILanguageConfigurationDto { @@ -397,6 +402,8 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void; $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; + $registerInlineHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; + $emitInlineHintsEvent(eventHandle: number, event?: any): void; $registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void; $registerDocumentColorProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; @@ -415,6 +422,7 @@ export interface MainThreadLanguagesShape extends IDisposable { export interface MainThreadMessageOptions { extension?: IExtensionDescription; modal?: boolean; + useCustom?: boolean; } export interface MainThreadMessageServiceShape extends IDisposable { @@ -438,6 +446,16 @@ export interface MainThreadProgressShape extends IDisposable { $progressEnd(handle: number): void; } +/** + * A terminal that is created on the extension host side is temporarily assigned + * a UUID by the extension host that created it. Once the renderer side has assigned + * a real numeric id, the numeric id will be used. + * + * All other terminals (that are not created on the extension host side) always + * use the numeric id. + */ +export type TerminalIdentifier = number | string; + export interface TerminalLaunchConfig { name?: string; shellPath?: string; @@ -452,11 +470,11 @@ export interface TerminalLaunchConfig { } export interface MainThreadTerminalServiceShape extends IDisposable { - $createTerminal(config: TerminalLaunchConfig): Promise<{ id: number, name: string; }>; - $dispose(terminalId: number): void; - $hide(terminalId: number): void; - $sendText(terminalId: number, text: string, addNewLine: boolean): void; - $show(terminalId: number, preserveFocus: boolean): void; + $createTerminal(extHostTerminalId: string, config: TerminalLaunchConfig): Promise; + $dispose(id: TerminalIdentifier): void; + $hide(id: TerminalIdentifier): void; + $sendText(id: TerminalIdentifier, text: string, addNewLine: boolean): void; + $show(id: TerminalIdentifier, preserveFocus: boolean): void; $startSendingDataEvents(): void; $stopSendingDataEvents(): void; $startLinkProvider(): void; @@ -594,6 +612,24 @@ export interface ExtHostEditorInsetsShape { $onDidReceiveMessage(handle: number, message: any): void; } +//#region --- open editors model + +export interface MainThreadEditorTabsShape extends IDisposable { + // manage tabs: move, close, rearrange etc +} + +export interface IEditorTabDto { + group: number; + name: string; + resource: UriComponents +} + +export interface IExtHostEditorTabsShape { + $acceptEditorTabs(tabs: IEditorTabDto[]): void; +} + +//#endregion + export type WebviewHandle = string; export interface WebviewPanelShowOptions { @@ -739,6 +775,7 @@ export enum NotebookEditorRevealType { Default = 0, InCenter = 1, InCenterIfOutsideViewport = 2, + AtTop = 3 } export interface INotebookDocumentShowOptions { @@ -786,6 +823,16 @@ export interface ExtHostUrlsShape { $handleExternalUri(handle: number, uri: UriComponents): Promise; } +export interface MainThreadUriOpenersShape extends IDisposable { + $registerUriOpener(id: string, schemes: readonly string[], extensionId: ExtensionIdentifier, label: string): Promise; + $unregisterUriOpener(id: string): Promise; +} + +export interface ExtHostUriOpenersShape { + $canOpenUri(id: string, uri: UriComponents, token: CancellationToken): Promise; + $openUri(id: string, context: { resolvedUri: UriComponents, sourceUri: UriComponents }, token: CancellationToken): Promise; +} + export interface ITextSearchComplete { limitHit?: boolean; } @@ -853,6 +900,7 @@ export interface MainThreadExtensionServiceShape extends IDisposable { $onExtensionActivationError(extensionId: ExtensionIdentifier, error: ExtensionActivationError): Promise; $onExtensionRuntimeError(extensionId: ExtensionIdentifier, error: SerializedError): void; $onExtensionHostExit(code: number): Promise; + $setPerformanceMarks(marks: performance.PerformanceMark[]): Promise; } export interface SCMProviderFeatures { @@ -946,6 +994,7 @@ export interface MainThreadDebugServiceShape extends IDisposable { export interface IOpenUriOptions { readonly allowTunneling?: boolean; + readonly allowContributedOpeners?: boolean | string; } export interface MainThreadWindowShape extends IDisposable { @@ -958,8 +1007,9 @@ export interface MainThreadTunnelServiceShape extends IDisposable { $openTunnel(tunnelOptions: TunnelOptions, source: string | undefined): Promise; $closeTunnel(remote: { host: string, port: number }): Promise; $getTunnels(): Promise; - $setTunnelProvider(): Promise; - $tunnelServiceReady(): Promise; + $setTunnelProvider(features: TunnelProviderFeatures): Promise; + $setCandidateFinder(): Promise; + $setCandidateFilter(): Promise; $onFoundNewCandidates(candidates: { host: string, port: number, detail: string }[]): Promise; } @@ -1052,7 +1102,7 @@ export interface ExtHostTreeViewsShape { $setSelection(treeViewId: string, treeItemHandles: string[]): void; $setVisible(treeViewId: string, visible: boolean): void; $hasResolve(treeViewId: string): Promise; - $resolve(treeViewId: string, treeItemHandle: string): Promise; + $resolve(treeViewId: string, treeItemHandle: string, token: CancellationToken): Promise; } export interface ExtHostWorkspaceShape { @@ -1094,7 +1144,10 @@ export interface ExtHostAuthenticationShape { $onDidChangeAuthenticationSessions(id: string, label: string, event: modes.AuthenticationSessionsChangeEvent): Promise; $onDidChangeAuthenticationProviders(added: modes.AuthenticationProviderInformation[], removed: modes.AuthenticationProviderInformation[]): Promise; $setProviders(providers: modes.AuthenticationProviderInformation[]): Promise; - $onDidChangePassword(): Promise; +} + +export interface ExtHostSecretStateShape { + $onDidChangePassword(e: { extensionId: string, key: string }): Promise; } export interface ExtHostSearchShape { @@ -1145,9 +1198,14 @@ export interface SourceTargetPair { target: UriComponents; } +export interface IWillRunFileOperationParticipation { + edit: IWorkspaceEditDto; + extensionNames: string[] +} + export interface ExtHostFileSystemEventServiceShape { $onFileEvent(events: FileSystemEvents): void; - $onWillRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[], undoRedoGroupId: number | undefined, timeout: number, token: CancellationToken): Promise; + $onWillRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise; $onDidRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[]): void; } @@ -1252,6 +1310,18 @@ export interface ISignatureHelpContextDto { readonly activeSignatureHelp?: ISignatureHelpDto; } +export interface IInlineHintDto { + text: string; + range: IRange; + hoverMessage?: string; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; +} + +export interface IInlineHintsDto { + hints: IInlineHintDto[] +} + export interface ILocationDto { uri: UriComponents; range: IRange; @@ -1441,6 +1511,7 @@ export interface ExtHostLanguageFeaturesShape { $releaseCompletionItems(handle: number, id: number): void; $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Promise; $releaseSignatureHelp(handle: number, id: number): void; + $provideInlineHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise $provideDocumentLinks(handle: number, resource: UriComponents, token: CancellationToken): Promise; $resolveDocumentLink(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; $releaseDocumentLinks(handle: number, id: number): void; @@ -1473,6 +1544,7 @@ export interface IShellLaunchConfigDto { cwd?: string | UriComponents; env?: { [key: string]: string | null; }; hideFromUser?: boolean; + flowControl?: boolean; } export interface IShellDefinitionDto { @@ -1503,7 +1575,7 @@ export interface ITerminalDimensionsDto { export interface ExtHostTerminalServiceShape { $acceptTerminalClosed(id: number, exitCode: number | undefined): void; - $acceptTerminalOpened(id: number, name: string, shellLaunchConfig: IShellLaunchConfigDto): void; + $acceptTerminalOpened(id: number, extHostTerminalId: string | undefined, name: string, shellLaunchConfig: IShellLaunchConfigDto): void; $acceptActiveTerminalChanged(id: number | null): void; $acceptTerminalProcessId(id: number, processId: number): void; $acceptTerminalProcessData(id: number, data: string): void; @@ -1512,6 +1584,7 @@ export interface ExtHostTerminalServiceShape { $acceptTerminalMaximumDimensions(id: number, cols: number, rows: number): void; $spawnExtHostProcess(id: number, shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: UriComponents | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise; $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise; + $acceptProcessAckDataEvent(id: number, charCount: number): void; $acceptProcessInput(id: number, data: string): void; $acceptProcessResize(id: number, cols: number, rows: number): void; $acceptProcessShutdown(id: number, immediate: boolean): void; @@ -1725,7 +1798,7 @@ export interface ExtHostNotebookShape { $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise; $backup(viewType: string, uri: UriComponents, cancellation: CancellationToken): Promise; $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; - $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined }): void; + $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelFriendlyId: string | undefined }): void; $onDidReceiveMessage(editorId: string, rendererId: string | undefined, message: unknown): void; $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEventDto, isDirty: boolean): void; $acceptModelSaved(uriComponents: UriComponents): void; @@ -1749,9 +1822,11 @@ export interface MainThreadThemingShape extends IDisposable { } export interface ExtHostTunnelServiceShape { - $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined; + $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise; $closeTunnel(remote: { host: string, port: number }, silent?: boolean): Promise; $onDidTunnelsChange(): Promise; + $registerCandidateFinder(enable: boolean): Promise; + $applyCandidateFilter(candidates: CandidatePort[]): Promise; } export interface ExtHostTimelineShape { @@ -1764,11 +1839,12 @@ export const enum ExtHostTestingResource { } export interface ExtHostTestingShape { - $runTestsForProvider(req: RunTestForProviderRequest): Promise; + $runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise; $subscribeToTests(resource: ExtHostTestingResource, uri: UriComponents): void; $unsubscribeFromTests(resource: ExtHostTestingResource, uri: UriComponents): void; - + $lookupTest(test: TestIdWithProvider): Promise; $acceptDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void; + $publishTestResults(results: InternalTestResults): void; } export interface MainThreadTestingShape { @@ -1777,7 +1853,7 @@ export interface MainThreadTestingShape { $subscribeToDiffs(resource: ExtHostTestingResource, uri: UriComponents): void; $unsubscribeFromDiffs(resource: ExtHostTestingResource, uri: UriComponents): void; $publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void; - $runTests(req: RunTestsRequest): Promise; + $runTests(req: RunTestsRequest, token: CancellationToken): Promise; } // --- proxy identifiers @@ -1798,6 +1874,7 @@ export const MainContext = { MainThreadDocumentContentProviders: createMainId('MainThreadDocumentContentProviders'), MainThreadTextEditors: createMainId('MainThreadTextEditors'), MainThreadEditorInsets: createMainId('MainThreadEditorInsets'), + MainThreadEditorTabs: createMainId('MainThreadEditorTabs'), MainThreadErrors: createMainId('MainThreadErrors'), MainThreadTreeViews: createMainId('MainThreadTreeViews'), MainThreadDownloadService: createMainId('MainThreadDownloadService'), @@ -1810,6 +1887,7 @@ export const MainContext = { MainThreadProgress: createMainId('MainThreadProgress'), MainThreadQuickOpen: createMainId('MainThreadQuickOpen'), MainThreadStatusBar: createMainId('MainThreadStatusBar'), + MainThreadSecretState: createMainId('MainThreadSecretState'), MainThreadStorage: createMainId('MainThreadStorage'), MainThreadTelemetry: createMainId('MainThreadTelemetry'), MainThreadTerminalService: createMainId('MainThreadTerminalService'), @@ -1818,6 +1896,7 @@ export const MainContext = { MainThreadWebviewViews: createMainId('MainThreadWebviewViews'), MainThreadCustomEditors: createMainId('MainThreadCustomEditors'), MainThreadUrls: createMainId('MainThreadUrls'), + MainThreadUriOpeners: createMainId('MainThreadUriOpeners'), MainThreadWorkspace: createMainId('MainThreadWorkspace'), MainThreadFileSystem: createMainId('MainThreadFileSystem'), MainThreadExtensionService: createMainId('MainThreadExtensionService'), @@ -1863,10 +1942,13 @@ export const ExtHostContext = { ExtHostCustomEditors: createExtId('ExtHostCustomEditors'), ExtHostWebviewViews: createExtId('ExtHostWebviewViews'), ExtHostEditorInsets: createExtId('ExtHostEditorInsets'), + ExtHostEditorTabs: createExtId('ExtHostEditorTabs'), ExtHostProgress: createMainId('ExtHostProgress'), ExtHostComments: createMainId('ExtHostComments'), + ExtHostSecretState: createMainId('ExtHostSecretState'), ExtHostStorage: createMainId('ExtHostStorage'), ExtHostUrls: createExtId('ExtHostUrls'), + ExtHostUriOpeners: createExtId('ExtHostUriOpeners'), ExtHostOutputService: createMainId('ExtHostOutputService'), ExtHosLabelService: createMainId('ExtHostLabelService'), ExtHostNotebook: createMainId('ExtHostNotebook'), diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 08b2543a4..93f77a957 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -20,6 +20,8 @@ import { IRange } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; import { TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; //#region --- NEW world @@ -176,6 +178,57 @@ const newCommands: ApiCommand[] = [ [ApiCommandArgument.Uri, ApiCommandArgument.Number.with('linkResolveCount', 'Number of links that should be resolved, only when links are unresolved.').optional()], new ApiCommandResult('A promise that resolves to an array of DocumentLink-instances.', value => value.map(typeConverters.DocumentLink.to)) ), + // --- semantic tokens + new ApiCommand( + 'vscode.provideDocumentSemanticTokensLegend', '_provideDocumentSemanticTokensLegend', 'Provide semantic tokens legend for a document', + [ApiCommandArgument.Uri], + new ApiCommandResult('A promise that resolves to SemanticTokensLegend.', value => { + if (!value) { + return undefined; + } + return new types.SemanticTokensLegend(value.tokenTypes, value.tokenModifiers); + }) + ), + new ApiCommand( + 'vscode.provideDocumentSemanticTokens', '_provideDocumentSemanticTokens', 'Provide semantic tokens for a document', + [ApiCommandArgument.Uri], + new ApiCommandResult('A promise that resolves to SemanticTokens.', value => { + if (!value) { + return undefined; + } + const semanticTokensDto = decodeSemanticTokensDto(value); + if (semanticTokensDto.type !== 'full') { + // only accepting full semantic tokens from provideDocumentSemanticTokens + return undefined; + } + return new types.SemanticTokens(semanticTokensDto.data, undefined); + }) + ), + new ApiCommand( + 'vscode.provideDocumentRangeSemanticTokensLegend', '_provideDocumentRangeSemanticTokensLegend', 'Provide semantic tokens legend for a document range', + [ApiCommandArgument.Uri], + new ApiCommandResult('A promise that resolves to SemanticTokensLegend.', value => { + if (!value) { + return undefined; + } + return new types.SemanticTokensLegend(value.tokenTypes, value.tokenModifiers); + }) + ), + new ApiCommand( + 'vscode.provideDocumentRangeSemanticTokens', '_provideDocumentRangeSemanticTokens', 'Provide semantic tokens for a document range', + [ApiCommandArgument.Uri, ApiCommandArgument.Range], + new ApiCommandResult('A promise that resolves to SemanticTokens.', value => { + if (!value) { + return undefined; + } + const semanticTokensDto = decodeSemanticTokensDto(value); + if (semanticTokensDto.type !== 'full') { + // only accepting full semantic tokens from provideDocumentRangeSemanticTokens + return undefined; + } + return new types.SemanticTokens(semanticTokensDto.data, undefined); + }) + ), // --- completions new ApiCommand( 'vscode.executeCompletionItemProvider', '_executeCompletionItemProvider', 'Execute completion item provider.', @@ -271,13 +324,21 @@ const newCommands: ApiCommand[] = [ return []; }) ), + // --- inline hints + new ApiCommand( + 'vscode.executeInlineHintProvider', '_executeInlineHintProvider', 'Execute inline hints provider', + [ApiCommandArgument.Uri, ApiCommandArgument.Range], + new ApiCommandResult('A promise that resolves to an array of InlineHint objects', result => { + return result.map(typeConverters.InlineHint.to); + }) + ), // --- notebooks new ApiCommand( 'vscode.resolveNotebookContentProviders', '_resolveNotebookContentProvider', 'Resolve Notebook Content Providers', [ - new ApiCommandArgument('viewType', '', v => typeof v === 'string', v => v), - new ApiCommandArgument('displayName', '', v => typeof v === 'string', v => v), - new ApiCommandArgument('options', '', v => typeof v === 'object', v => v), + // new ApiCommandArgument('viewType', '', v => typeof v === 'string', v => v), + // new ApiCommandArgument('displayName', '', v => typeof v === 'string', v => v), + // new ApiCommandArgument('options', '', v => typeof v === 'object', v => v), ], new ApiCommandResult<{ viewType: string; diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 631999062..7376e9c66 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -15,11 +15,15 @@ interface GetSessionsRequest { result: Promise; } +interface ProviderWithMetadata { + label: string; + provider: vscode.AuthenticationProvider; + options: vscode.AuthenticationProviderOptions; +} + export class ExtHostAuthentication implements ExtHostAuthenticationShape { private _proxy: MainThreadAuthenticationShape; - private _authenticationProviders: Map = new Map(); - - private _providerIds: string[] = []; + private _authenticationProviders: Map = new Map(); private _providers: vscode.AuthenticationProviderInformation[] = []; @@ -29,9 +33,6 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { private _onDidChangeSessions = new Emitter(); readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; - private _onDidChangePassword = new Emitter(); - readonly onDidChangePassword: Event = this._onDidChangePassword.event; - private _inFlightRequests = new Map(); constructor(mainContext: IMainContext) { @@ -43,14 +44,6 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { return Promise.resolve(); } - getProviderIds(): Promise> { - return this._proxy.$getProviderIds(); - } - - get providerIds(): string[] { - return this._providerIds; - } - get providers(): ReadonlyArray { return Object.freeze(this._providers.slice()); } @@ -90,128 +83,120 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { private async _getSession(requestingExtension: IExtensionDescription, extensionId: string, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { await this._proxy.$ensureProvider(providerId); - const provider = this._authenticationProviders.get(providerId); + const providerData = this._authenticationProviders.get(providerId); const extensionName = requestingExtension.displayName || requestingExtension.name; - if (!provider) { + if (!providerData) { return this._proxy.$getSession(providerId, scopes, extensionId, extensionName, options); } const orderedScopes = scopes.sort().join(' '); - const sessions = (await provider.getSessions()).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes); + const sessions = (await providerData.provider.getSessions()).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes); let session: vscode.AuthenticationSession | undefined = undefined; if (sessions.length) { - if (!provider.supportsMultipleAccounts) { + if (!providerData.options.supportsMultipleAccounts) { session = sessions[0]; - const allowed = await this._proxy.$getSessionsPrompt(providerId, session.account.label, provider.label, extensionId, extensionName); + const allowed = await this._proxy.$getSessionsPrompt(providerId, session.account.label, providerData.label, extensionId, extensionName); if (!allowed) { throw new Error('User did not consent to login.'); } } else { // On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid - const selected = await this._proxy.$selectSession(providerId, provider.label, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); + const selected = await this._proxy.$selectSession(providerId, providerData.label, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); session = sessions.find(session => session.id === selected.id); } } else { if (options.createIfNone) { - const isAllowed = await this._proxy.$loginPrompt(provider.label, extensionName); + const isAllowed = await this._proxy.$loginPrompt(providerData.label, extensionName); if (!isAllowed) { throw new Error('User did not consent to login.'); } - session = await provider.login(scopes); + session = await providerData.provider.login(scopes); await this._proxy.$setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id); } else { await this._proxy.$requestNewSession(providerId, scopes, extensionId, extensionName); } } - return session; } async logout(providerId: string, sessionId: string): Promise { - const provider = this._authenticationProviders.get(providerId); - if (!provider) { + const providerData = this._authenticationProviders.get(providerId); + if (!providerData) { return this._proxy.$logout(providerId, sessionId); } - return provider.logout(sessionId); + return providerData.provider.logout(sessionId); } - registerAuthenticationProvider(provider: vscode.AuthenticationProvider): vscode.Disposable { - if (this._authenticationProviders.get(provider.id)) { - throw new Error(`An authentication provider with id '${provider.id}' is already registered.`); + registerAuthenticationProvider(id: string, label: string, provider: vscode.AuthenticationProvider, options?: vscode.AuthenticationProviderOptions): vscode.Disposable { + if (this._authenticationProviders.get(id)) { + throw new Error(`An authentication provider with id '${id}' is already registered.`); } - this._authenticationProviders.set(provider.id, provider); - if (!this._providerIds.includes(provider.id)) { - this._providerIds.push(provider.id); - } + this._authenticationProviders.set(id, { label, provider, options: options ?? { supportsMultipleAccounts: false } }); - if (!this._providers.find(p => p.id === provider.id)) { + if (!this._providers.find(p => p.id === id)) { this._providers.push({ - id: provider.id, - label: provider.label + id: id, + label: label }); } const listener = provider.onDidChangeSessions(e => { - this._proxy.$sendDidChangeSessions(provider.id, e); + this._proxy.$sendDidChangeSessions(id, e); }); - this._proxy.$registerAuthenticationProvider(provider.id, provider.label, provider.supportsMultipleAccounts); + this._proxy.$registerAuthenticationProvider(id, label, options?.supportsMultipleAccounts ?? false); return new Disposable(() => { listener.dispose(); - this._authenticationProviders.delete(provider.id); - const index = this._providerIds.findIndex(id => id === provider.id); - if (index > -1) { - this._providerIds.splice(index); - } + this._authenticationProviders.delete(id); - const i = this._providers.findIndex(p => p.id === provider.id); + const i = this._providers.findIndex(p => p.id === id); if (i > -1) { this._providers.splice(i); } - this._proxy.$unregisterAuthenticationProvider(provider.id); + this._proxy.$unregisterAuthenticationProvider(id); }); } $login(providerId: string, scopes: string[]): Promise { - const authProvider = this._authenticationProviders.get(providerId); - if (authProvider) { - return Promise.resolve(authProvider.login(scopes)); + const providerData = this._authenticationProviders.get(providerId); + if (providerData) { + return Promise.resolve(providerData.provider.login(scopes)); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } $logout(providerId: string, sessionId: string): Promise { - const authProvider = this._authenticationProviders.get(providerId); - if (authProvider) { - return Promise.resolve(authProvider.logout(sessionId)); + const providerData = this._authenticationProviders.get(providerId); + if (providerData) { + return Promise.resolve(providerData.provider.logout(sessionId)); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } $getSessions(providerId: string): Promise> { - const authProvider = this._authenticationProviders.get(providerId); - if (authProvider) { - return Promise.resolve(authProvider.getSessions()); + const providerData = this._authenticationProviders.get(providerId); + if (providerData) { + return Promise.resolve(providerData.provider.getSessions()); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } async $getSessionAccessToken(providerId: string, sessionId: string): Promise { - const authProvider = this._authenticationProviders.get(providerId); - if (authProvider) { - const sessions = await authProvider.getSessions(); + const providerData = this._authenticationProviders.get(providerId); + if (providerData) { + const sessions = await providerData.provider.getSessions(); const session = sessions.find(session => session.id === sessionId); if (session) { return session.accessToken; @@ -245,23 +230,4 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { this._onDidChangeAuthenticationProviders.fire({ added, removed }); return Promise.resolve(); } - - async $onDidChangePassword(): Promise { - this._onDidChangePassword.fire(); - } - - getPassword(requestingExtension: IExtensionDescription, key: string): Promise { - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); - return this._proxy.$getPassword(extensionId, key); - } - - setPassword(requestingExtension: IExtensionDescription, key: string, value: string): Promise { - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); - return this._proxy.$setPassword(extensionId, key, value); - } - - deletePassword(requestingExtension: IExtensionDescription, key: string): Promise { - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); - return this._proxy.$deletePassword(extensionId, key); - } } diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 649612e38..c33737246 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -103,7 +103,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { const internalArgs = apiCommand.args.map((arg, i) => { if (!arg.validate(apiArgs[i])) { - throw new Error(`Invalid argument '${arg.name}' when running '${apiCommand.id}', receieved: ${apiArgs[i]}`); + throw new Error(`Invalid argument '${arg.name}' when running '${apiCommand.id}', received: ${apiArgs[i]}`); } return arg.convert(apiArgs[i]); }); @@ -194,7 +194,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { } } - private _executeContributedCommand(id: string, args: any[]): Promise { + private async _executeContributedCommand(id: string, args: any[]): Promise { const command = this._commands.get(id); if (!command) { throw new Error('Unknown command'); @@ -205,17 +205,16 @@ export class ExtHostCommands implements ExtHostCommandsShape { try { validateConstraint(args[i], description.args[i].constraint); } catch (err) { - return Promise.reject(new Error(`Running the contributed command: '${id}' failed. Illegal argument '${description.args[i].name}' - ${description.args[i].description}`)); + throw new Error(`Running the contributed command: '${id}' failed. Illegal argument '${description.args[i].name}' - ${description.args[i].description}`); } } } try { - const result = callback.apply(thisArg, args); - return Promise.resolve(result); + return await callback.apply(thisArg, args); } catch (err) { this._logService.error(err, id); - return Promise.reject(new Error(`Running the contributed command: '${id}' failed.`)); + throw new Error(`Running the contributed command: '${id}' failed.`); } } diff --git a/src/vs/workbench/api/common/extHostCustomEditors.ts b/src/vs/workbench/api/common/extHostCustomEditors.ts index becd0887e..a4359d91a 100644 --- a/src/vs/workbench/api/common/extHostCustomEditors.ts +++ b/src/vs/workbench/api/common/extHostCustomEditors.ts @@ -13,6 +13,7 @@ import * as modes from 'vs/editor/common/modes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; +import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; import { EditorGroupColumn } from 'vs/workbench/common/editor'; @@ -178,7 +179,7 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean }, ): vscode.Disposable { const disposables = new DisposableStore(); - if ('resolveCustomTextEditor' in provider) { + if (isCustomTextEditorProvider(provider)) { disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider)); this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, { supportsMove: !!provider.moveCustomTextEditor, @@ -208,7 +209,6 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor })); } - async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken) { const entry = this._editorProviders.get(viewType); if (!entry) { @@ -261,8 +261,10 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor throw new Error(`No provider found for '${viewType}'`); } + const viewColumn = typeConverters.ViewColumn.to(position); + const webview = this._extHostWebview.createNewWebview(handle, options, entry.extension); - const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, title, position, options, webview); + const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, title, viewColumn, options, webview); const revivedResource = URI.revive(resource); @@ -350,7 +352,6 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor return backup.id; } - private getCustomDocumentEntry(viewType: string, resource: UriComponents): CustomDocumentStoreEntry { const entry = this._documents.get(viewType, URI.revive(resource)); if (!entry) { @@ -375,6 +376,9 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor } } +function isCustomTextEditorProvider(provider: vscode.CustomReadonlyEditorProvider | vscode.CustomTextEditorProvider): provider is vscode.CustomTextEditorProvider { + return typeof (provider as vscode.CustomTextEditorProvider).resolveCustomTextEditor === 'function'; +} function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomDocumentEditEvent): e is vscode.CustomDocumentEditEvent { return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function' diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 4026a1d2b..d639431a1 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -152,7 +152,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E const source = src; - if (typeof source.sourceReference === 'number') { + if (typeof source.sourceReference === 'number' && source.sourceReference > 0) { // src can be retrieved via DAP's "source" request let debug = `debug:${encodeURIComponent(source.path || '')}`; diff --git a/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts new file mode 100644 index 000000000..2d0f7b4c6 --- /dev/null +++ b/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { IEditorTabDto, IExtHostEditorTabsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { URI } from 'vs/base/common/uri'; +import { Emitter, Event } from 'vs/base/common/event'; + + +export interface IEditorTab { + name: string; + group: number; + resource: vscode.Uri +} + +export class ExtHostEditorTabs implements IExtHostEditorTabsShape { + + private readonly _onDidChangeTabs = new Emitter(); + readonly onDidChangeTabs: Event = this._onDidChangeTabs.event; + + private _tabs: IEditorTab[] = []; + + get tabs(): readonly IEditorTab[] { + return this._tabs; + } + + $acceptEditorTabs(tabs: IEditorTabDto[]): void { + this._tabs = tabs.map(dto => { + return { + name: dto.name, + group: dto.group, + resource: URI.revive(dto.resource) + }; + }); + this._onDidChangeTabs.fire(); + } +} diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 328b93272..2274a4f12 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -5,6 +5,7 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; +import * as performance from 'vs/base/common/performance'; import { originalFSPath, joinPath } from 'vs/base/common/resources'; import { Barrier, timeout } from 'vs/base/common/async'; import { dispose, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; @@ -35,6 +36,8 @@ import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelServ import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; import { Emitter, Event } from 'vs/base/common/event'; import { IExtensionActivationHost, checkActivateWorkspaceContainsExtension } from 'vs/workbench/api/common/shared/workspaceContains'; +import { ExtHostSecretState, IExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; +import { ExtensionSecrets } from 'vs/workbench/api/common/extHostSecrets'; interface ITestRunner { /** Old test runner API, as exported from `vscode/lib/testrunner` */ @@ -50,7 +53,7 @@ export const IHostUtils = createDecorator('IHostUtils'); export interface IHostUtils { readonly _serviceBrand: undefined; - exit(code?: number): void; + exit(code: number): void; exists(path: string): Promise; realpath(path: string): Promise; } @@ -94,6 +97,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme private readonly _readyToRunExtensions: Barrier; protected readonly _registry: ExtensionDescriptionRegistry; private readonly _storage: ExtHostStorage; + private readonly _secretState: ExtHostSecretState; private readonly _storagePath: IExtensionStoragePaths; private readonly _activator: ExtensionsActivator; private _extensionPathIndex: Promise> | null; @@ -115,7 +119,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme @IExtHostInitDataService initData: IExtHostInitDataService, @IExtensionStoragePaths storagePath: IExtensionStoragePaths, @IExtHostTunnelService extHostTunnelService: IExtHostTunnelService, - @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService + @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService, ) { super(); this._hostUtils = hostUtils; @@ -138,10 +142,12 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._readyToRunExtensions = new Barrier(); this._registry = new ExtensionDescriptionRegistry(this._initData.extensions); this._storage = new ExtHostStorage(this._extHostContext); + this._secretState = new ExtHostSecretState(this._extHostContext); this._storagePath = storagePath; this._instaService = instaService.createChild(new ServiceCollection( - [IExtHostStorage, this._storage] + [IExtHostStorage, this._storage], + [IExtHostSecretState, this._secretState] )); const hostExtensions = new Set(); @@ -184,6 +190,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._almostReadyToRunExtensions.open(); await this._extHostWorkspace.waitForInitializeCall(); + performance.mark('code/extHost/ready'); this._readyToStartExtensionHost.open(); if (this._initData.autoStart) { @@ -362,10 +369,14 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup); return Promise.all([ - this._loadCommonJSModule(joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder), + this._loadCommonJSModule(extensionDescription.identifier, joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder), this._loadExtensionContext(extensionDescription) ]).then(values => { + performance.mark(`code/extHost/willActivateExtension/${extensionDescription.identifier.value}`); return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder); + }).then((activatedExtension) => { + performance.mark(`code/extHost/didActivateExtension/${extensionDescription.identifier.value}`); + return activatedExtension; }); } @@ -373,6 +384,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const globalState = new ExtensionGlobalMemento(extensionDescription, this._storage); const workspaceState = new ExtensionMemento(extensionDescription.identifier.value, false, this._storage); + const secrets = new ExtensionSecrets(extensionDescription, this._secretState); const extensionMode = extensionDescription.isUnderDevelopment ? (this._initData.environment.extensionTestsLocationURI ? ExtensionMode.Test : ExtensionMode.Development) : ExtensionMode.Production; @@ -388,6 +400,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return Object.freeze({ globalState, workspaceState, + secrets, subscriptions: [], get extensionUri() { return extensionDescription.extensionLocation; }, get extensionPath() { return extensionDescription.extensionLocation.fsPath; }, @@ -456,6 +469,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } private _activateAllStartupFinished(): void { + // startup is considered finished + this._mainThreadExtensionsProxy.$setPerformanceMarks(performance.getMarks()); + for (const desc of this._registry.getAllExtensionDescriptions()) { if (desc.activationEvents) { for (const activationEvent of desc.activationEvents) { @@ -541,7 +557,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme let testRunner: ITestRunner | INewTestRunner | undefined; let requireError: Error | undefined; try { - testRunner = await this._loadCommonJSModule(URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false)); + testRunner = await this._loadCommonJSModule(null, URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false)); } catch (error) { requireError = error; } @@ -586,11 +602,17 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } private _testRunnerExit(code: number): void { + this._logService.info(`extension host terminating: test runner requested exit with code ${code}`); + this._logService.flush(); + // wait at most 5000ms for the renderer to confirm our exit request and for the renderer socket to drain // (this is to ensure all outstanding messages reach the renderer) const exitPromise = this._mainThreadExtensionsProxy.$onExtensionHostExit(code); const drainPromise = this._extHostContext.drain(); Promise.race([Promise.all([exitPromise, drainPromise]), timeout(5000)]).then(() => { + this._logService.info(`exiting with code ${code}`); + this._logService.flush(); + this._hostUtils.exit(code); }); } @@ -644,7 +666,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } try { + performance.mark(`code/extHost/willResolveAuthority/${authorityPrefix}`); const result = await resolver.resolve(remoteAuthority, { resolveAttempt }); + performance.mark(`code/extHost/didResolveAuthorityOK/${authorityPrefix}`); this._disposables.add(await this._extHostTunnelService.setTunnelExtensionFunctions(resolver)); // Split merged API result into separate authority/options @@ -667,6 +691,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } }; } catch (err) { + performance.mark(`code/extHost/didResolveAuthorityError/${authorityPrefix}`); if (err instanceof RemoteAuthorityResolverError) { return { type: 'error', @@ -754,7 +779,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme protected abstract _beforeAlmostReadyToRunExtensions(): Promise; protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined; - protected abstract _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; + protected abstract _loadCommonJSModule(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; public abstract $setRemoteEnvironment(env: { [key: string]: string | null }): Promise; } diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index 0503a131b..6801cf0b3 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -8,7 +8,7 @@ import { IRelativePattern, parse } from 'vs/base/common/glob'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import type * as vscode from 'vscode'; -import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, SourceTargetPair, IWorkspaceEditDto, MainThreadBulkEditsShape } from './extHost.protocol'; +import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, SourceTargetPair, IWorkspaceEditDto, IWillRunFileOperationParticipation } from './extHost.protocol'; import * as typeConverter from './extHostTypeConverters'; import { Disposable, WorkspaceEdit } from './extHostTypes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -122,8 +122,7 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ constructor( mainContext: IMainContext, private readonly _logService: ILogService, - private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, - private readonly _mainThreadBulkEdits: MainThreadBulkEditsShape = mainContext.getProxy(MainContext.MainThreadBulkEdits) + private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors ) { // } @@ -178,24 +177,21 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ }; } - async $onWillRunFileOperation(operation: FileOperation, files: SourceTargetPair[], undoRedoGroupId: number | undefined, timeout: number, token: CancellationToken): Promise { + async $onWillRunFileOperation(operation: FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise { switch (operation) { case FileOperation.MOVE: - await this._fireWillEvent(this._onWillRenameFile, { files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }, undoRedoGroupId, timeout, token); - break; + return await this._fireWillEvent(this._onWillRenameFile, { files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }, timeout, token); case FileOperation.DELETE: - await this._fireWillEvent(this._onWillDeleteFile, { files: files.map(f => URI.revive(f.target)) }, undoRedoGroupId, timeout, token); - break; + return await this._fireWillEvent(this._onWillDeleteFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token); case FileOperation.CREATE: - await this._fireWillEvent(this._onWillCreateFile, { files: files.map(f => URI.revive(f.target)) }, undoRedoGroupId, timeout, token); - break; - default: - //ignore, dont send + return await this._fireWillEvent(this._onWillCreateFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token); } + return undefined; } - private async _fireWillEvent(emitter: AsyncEmitter, data: Omit, undoRedoGroupId: number | undefined, timeout: number, token: CancellationToken): Promise { + private async _fireWillEvent(emitter: AsyncEmitter, data: Omit, timeout: number, token: CancellationToken): Promise { + const extensionNames = new Set(); const edits: WorkspaceEdit[] = []; await emitter.fireAsync(data, token, async (thenable, listener) => { @@ -204,25 +200,28 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ const result = await Promise.resolve(thenable); if (result instanceof WorkspaceEdit) { edits.push(result); + extensionNames.add((>listener).extension.displayName ?? (>listener).extension.identifier.value); } if (Date.now() - now > timeout) { - this._logService.warn('SLOW file-participant', (>listener).extension?.identifier); + this._logService.warn('SLOW file-participant', (>listener).extension.identifier); } }); if (token.isCancellationRequested) { - return; + return undefined; } - if (edits.length > 0) { - // concat all WorkspaceEdits collected via waitUntil-call and apply them in one go. - const dto: IWorkspaceEditDto = { edits: [] }; - for (let edit of edits) { - let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); - dto.edits = dto.edits.concat(edits); - } - return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit(dto, undoRedoGroupId); + if (edits.length === 0) { + return undefined; } + + // concat all WorkspaceEdits collected via waitUntil-call and send them over to the renderer + const dto: IWorkspaceEditDto = { edits: [] }; + for (let edit of edits) { + let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); + dto.edits = dto.edits.concat(edits); + } + return { edit: dto, extensionNames: Array.from(extensionNames) }; } } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 49cde88e1..34ca10650 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -27,11 +27,12 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { IURITransformer } from 'vs/base/common/uriIpc'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; -import { encodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto'; +import { encodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; import { IdGenerator } from 'vs/base/common/idGenerator'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { Cache } from './cache'; import { StopWatch } from 'vs/base/common/stopwatch'; +import { CancellationError } from 'vs/base/common/errors'; // --- adapter @@ -1062,6 +1063,20 @@ class SignatureHelpAdapter { } } +class InlineHintsAdapter { + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.InlineHintsProvider, + ) { } + + provideInlineHints(resource: URI, range: IRange, token: CancellationToken): Promise { + const doc = this._documents.getDocument(resource); + return asPromise(() => this._provider.provideInlineHints(doc, typeConvert.Range.to(range), token)).then(value => { + return value ? { hints: value.map(typeConvert.InlineHint.from) } : undefined; + }); + } +} + class LinkProviderAdapter { private _cache = new Cache('DocumentLink'); @@ -1320,7 +1335,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter - | LinkedEditingRangeAdapter; + | LinkedEditingRangeAdapter | InlineHintsAdapter; class AdapterData { constructor( @@ -1403,7 +1418,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return ExtHostLanguageFeatures._handlePool++; } - private _withAdapter(handle: number, ctor: { new(...args: any[]): A; }, callback: (adapter: A, extension: IExtensionDescription | undefined) => Promise, fallbackValue: R): Promise { + private _withAdapter(handle: number, ctor: { new(...args: any[]): A; }, callback: (adapter: A, extension: IExtensionDescription | undefined) => Promise, fallbackValue: R, allowCancellationError: boolean = false): Promise { const data = this._adapter.get(handle); if (!data) { return Promise.resolve(fallbackValue); @@ -1421,8 +1436,11 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF Promise.resolve(p).then( () => this._logService.trace(`[${extension.identifier.value}] provider DONE after ${Date.now() - t1}ms`), err => { - this._logService.error(`[${extension.identifier.value}] provider FAILED`); - this._logService.error(err); + const isExpectedError = allowCancellationError && (err instanceof CancellationError); + if (!isExpectedError) { + this._logService.error(`[${extension.identifier.value}] provider FAILED`); + this._logService.error(err); + } } ); } @@ -1711,7 +1729,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF } $provideDocumentSemanticTokens(handle: number, resource: UriComponents, previousResultId: number, token: CancellationToken): Promise { - return this._withAdapter(handle, DocumentSemanticTokensAdapter, adapter => adapter.provideDocumentSemanticTokens(URI.revive(resource), previousResultId, token), null); + return this._withAdapter(handle, DocumentSemanticTokensAdapter, adapter => adapter.provideDocumentSemanticTokens(URI.revive(resource), previousResultId, token), null, true); } $releaseDocumentSemanticTokens(handle: number, semanticColoringResultId: number): void { @@ -1725,7 +1743,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF } $provideDocumentRangeSemanticTokens(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise { - return this._withAdapter(handle, DocumentRangeSemanticTokensAdapter, adapter => adapter.provideDocumentRangeSemanticTokens(URI.revive(resource), range, token), null); + return this._withAdapter(handle, DocumentRangeSemanticTokensAdapter, adapter => adapter.provideDocumentRangeSemanticTokens(URI.revive(resource), range, token), null, true); } //#endregion @@ -1770,6 +1788,27 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._withAdapter(handle, SignatureHelpAdapter, adapter => adapter.releaseSignatureHelp(id), undefined); } + // --- inline hints + + registerInlineHintsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineHintsProvider): vscode.Disposable { + + const eventHandle = typeof provider.onDidChangeInlineHints === 'function' ? this._nextHandle() : undefined; + const handle = this._addNewAdapter(new InlineHintsAdapter(this._documents, provider), extension); + + this._proxy.$registerInlineHintsProvider(handle, this._transformDocumentSelector(selector), eventHandle); + let result = this._createDisposable(handle); + + if (eventHandle !== undefined) { + const subscription = provider.onDidChangeInlineHints!(_ => this._proxy.$emitInlineHintsEvent(eventHandle)); + result = Disposable.from(result, subscription); + } + return result; + } + + $provideInlineHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise { + return this._withAdapter(handle, InlineHintsAdapter, adapter => adapter.provideInlineHints(URI.revive(resource), range, token), undefined); + } + // --- links registerDocumentLinkProvider(extension: IExtensionDescription | undefined, selector: vscode.DocumentSelector, provider: vscode.DocumentLinkProvider): vscode.Disposable { @@ -1882,7 +1921,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return { beforeText: ExtHostLanguageFeatures._serializeRegExp(onEnterRule.beforeText), afterText: onEnterRule.afterText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.afterText) : undefined, - oneLineAboveText: onEnterRule.oneLineAboveText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.oneLineAboveText) : undefined, + previousLineText: onEnterRule.previousLineText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.previousLineText) : undefined, action: onEnterRule.action }; } diff --git a/src/vs/workbench/api/common/extHostMessageService.ts b/src/vs/workbench/api/common/extHostMessageService.ts index ff75e2f33..847a27a2b 100644 --- a/src/vs/workbench/api/common/extHostMessageService.ts +++ b/src/vs/workbench/api/common/extHostMessageService.ts @@ -8,6 +8,7 @@ import type * as vscode from 'vscode'; import { MainContext, MainThreadMessageServiceShape, MainThreadMessageOptions, IMainContext } from './extHost.protocol'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; function isMessageItem(item: any): item is vscode.MessageItem { return item && item.title; @@ -37,9 +38,14 @@ export class ExtHostMessageService { items = [optionsOrFirstItem, ...rest]; } else { options.modal = optionsOrFirstItem && optionsOrFirstItem.modal; + options.useCustom = optionsOrFirstItem && optionsOrFirstItem.useCustom; items = rest; } + if (options.useCustom) { + checkProposedApiEnabled(extension); + } + const commands: { title: string; isCloseAffordance: boolean; handle: number; }[] = []; for (let handle = 0; handle < items.length; handle++) { diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index bb136d1c0..298548f29 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -78,8 +78,8 @@ export interface ExtHostNotebookOutputRenderingHandler { } export class ExtHostNotebookKernelProviderAdapter extends Disposable { - private _kernelToId = new Map(); - private _idToKernel = new Map(); + private _kernelToFriendlyId = new Map(); + private _friendlyIdToKernel = new Map(); constructor( private readonly _proxy: MainThreadNotebookShape, private readonly _handle: number, @@ -101,24 +101,25 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { const newMap = new Map(); let kernel_unique_pool = 0; - const kernelIdCache = new Set(); + const kernelFriendlyIdCache = new Set(); const transformedData: INotebookKernelInfoDto2[] = data.map(kernel => { - let id = this._kernelToId.get(kernel); - if (id === undefined) { - if (kernel.id && kernelIdCache.has(kernel.id)) { - id = `${this._extension.identifier.value}_${kernel.id}_${kernel_unique_pool++}`; + let friendlyId = this._kernelToFriendlyId.get(kernel); + if (friendlyId === undefined) { + if (kernel.id && kernelFriendlyIdCache.has(kernel.id)) { + friendlyId = `${this._extension.identifier.value}_${kernel.id}_${kernel_unique_pool++}`; } else { - id = `${this._extension.identifier.value}_${kernel.id || UUID.generateUuid()}`; + friendlyId = `${this._extension.identifier.value}_${kernel.id || UUID.generateUuid()}`; } - this._kernelToId.set(kernel, id); + this._kernelToFriendlyId.set(kernel, friendlyId); } - newMap.set(kernel, id); + newMap.set(kernel, friendlyId); return { - id, + id: kernel.id, + friendlyId: friendlyId, label: kernel.label, extension: this._extension.identifier, extensionLocation: this._extension.extensionLocation, @@ -129,22 +130,22 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { }; }); - this._kernelToId = newMap; + this._kernelToFriendlyId = newMap; - this._idToKernel.clear(); - this._kernelToId.forEach((value, key) => { - this._idToKernel.set(value, key); + this._friendlyIdToKernel.clear(); + this._kernelToFriendlyId.forEach((value, key) => { + this._friendlyIdToKernel.set(value, key); }); return transformedData; } - getKernel(kernelId: string) { - return this._idToKernel.get(kernelId); + getKernelByFriendlyId(kernelId: string) { + return this._friendlyIdToKernel.get(kernelId); } async resolveNotebook(kernelId: string, document: ExtHostNotebookDocument, webview: vscode.NotebookCommunication, token: CancellationToken) { - const kernel = this._idToKernel.get(kernelId); + const kernel = this._friendlyIdToKernel.get(kernelId); if (kernel && this._provider.resolveKernel) { return this._provider.resolveKernel(kernel, document.notebookDocument, webview, token); @@ -152,7 +153,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } async executeNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined) { - const kernel = this._idToKernel.get(kernelId); + const kernel = this._friendlyIdToKernel.get(kernelId); if (!kernel) { return; @@ -166,7 +167,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } async cancelNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined) { - const kernel = this._idToKernel.get(kernelId); + const kernel = this._friendlyIdToKernel.get(kernelId); if (!kernel) { return; @@ -580,10 +581,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._outputDisplayOrder = displayOrder; } - $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined; }) { + $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelFriendlyId: string | undefined; }) { if (event.providerHandle !== undefined) { this._withAdapter(event.providerHandle, event.uri, async (adapter, document) => { - const kernel = event.kernelId ? adapter.getKernel(event.kernelId) : undefined; + const kernel = event.kernelFriendlyId ? adapter.getKernelByFriendlyId(event.kernelFriendlyId) : undefined; this._editors.forEach(editor => { if (editor.editor.notebookData === document) { editor.editor._acceptKernel(kernel); diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index e67332a40..85f9083ba 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -113,14 +113,17 @@ export class ExtHostCell extends Disposable { get cell(): vscode.NotebookCell { if (!this._cell) { const that = this; - const document = this._extHostDocument.getDocument(this.uri)!.document; + const data = this._extHostDocument.getDocument(this.uri); + if (!data) { + throw new Error(`MISSING extHostDocument for notebook cell: ${this.uri}`); + } this._cell = Object.freeze({ get index() { return that._notebook.getCellIndex(that); }, notebook: that._notebook.notebookDocument, uri: that.uri, cellKind: this._cellData.cellKind, - document, - get language() { return document.languageId; }, + document: data.document, + get language() { return data!.document.languageId; }, get outputs() { return that._outputs; }, set outputs(value) { that._updateOutputs(value); }, get metadata() { return that._metadata; }, diff --git a/src/vs/workbench/api/common/extHostRequireInterceptor.ts b/src/vs/workbench/api/common/extHostRequireInterceptor.ts index fd9ae5f2d..f59d41ce8 100644 --- a/src/vs/workbench/api/common/extHostRequireInterceptor.ts +++ b/src/vs/workbench/api/common/extHostRequireInterceptor.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as performance from 'vs/base/common/performance'; import { TernarySearchTree } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { MainThreadTelemetryShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; @@ -52,7 +53,9 @@ export abstract class RequireInterceptor { this._installInterceptor(); + performance.mark('code/extHost/willWaitForConfig'); const configProvider = await this._extHostConfiguration.getConfigProvider(); + performance.mark('code/extHost/didWaitForConfig'); const extensionPaths = await this._extHostExtensionService.getExtensionPathIndex(); this.register(new VSCodeNodeModuleFactory(this._apiFactory, extensionPaths, this._extensionRegistry, configProvider, this._logService)); diff --git a/src/vs/workbench/api/common/extHostSecrets.ts b/src/vs/workbench/api/common/extHostSecrets.ts new file mode 100644 index 000000000..6d973d31e --- /dev/null +++ b/src/vs/workbench/api/common/extHostSecrets.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; + +import { ExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { Emitter, Event } from 'vs/base/common/event'; + +export class ExtensionSecrets implements vscode.SecretStorage { + + protected readonly _id: string; + readonly #secretState: ExtHostSecretState; + + private _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + + constructor(extensionDescription: IExtensionDescription, secretState: ExtHostSecretState) { + this._id = ExtensionIdentifier.toKey(extensionDescription.identifier); + this.#secretState = secretState; + + this.#secretState.onDidChangePassword(e => { + if (e.extensionId === this._id) { + this._onDidChange.fire({ key: e.key }); + } + }); + } + + get(key: string): Promise { + return this.#secretState.get(this._id, key); + } + + store(key: string, value: string): Promise { + return this.#secretState.store(this._id, key, value); + } + + delete(key: string): Promise { + return this.#secretState.delete(this._id, key); + } +} diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index a89e9fe30..877d56e79 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -10,8 +10,6 @@ import { MainContext, MainThreadStatusBarShape, IMainContext, ICommandDto } from import { localize } from 'vs/nls'; import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private static ID_GEN = 0; @@ -48,9 +46,8 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private _proxy: MainThreadStatusBarShape; private _commands: CommandsConverter; private _accessibilityInformation?: vscode.AccessibilityInformation; - private _extension?: IExtensionDescription; - constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation, extension?: IExtensionDescription) { + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation) { this._id = ExtHostStatusBarEntry.ID_GEN++; this._proxy = proxy; this._commands = commands; @@ -59,7 +56,6 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { this._alignment = alignment; this._priority = priority; this._accessibilityInformation = accessibilityInformation; - this._extension = extension; } public get id(): number { @@ -87,10 +83,6 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { } public get backgroundColor(): ThemeColor | undefined { - if (this._extension) { - checkProposedApiEnabled(this._extension); - } - return this._backgroundColor; } @@ -118,10 +110,6 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { } public set backgroundColor(color: ThemeColor | undefined) { - if (this._extension) { - checkProposedApiEnabled(this._extension); - } - if (color && !ExtHostStatusBarEntry.ALLOWED_BACKGROUND_COLORS.has(color.id)) { color = undefined; } @@ -248,8 +236,8 @@ export class ExtHostStatusBar { this._statusMessage = new StatusBarMessage(this); } - createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation, extension?: IExtensionDescription): vscode.StatusBarItem { - return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority, accessibilityInformation, extension); + createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation): vscode.StatusBarItem { + return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority, accessibilityInformation); } setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): Disposable { diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 1cccd958f..f684dbb85 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -5,12 +5,11 @@ import type * as vscode from 'vscode'; import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto, ITerminalDimensionsDto, ITerminalLinkDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto, ITerminalDimensionsDto, ITerminalLinkDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { ITerminalChildProcess, EXT_HOST_CREATION_DELAY, ITerminalLaunchError, ITerminalDimensionsOverride } from 'vs/workbench/contrib/terminal/common/terminal'; -import { timeout } from 'vs/base/common/async'; +import { ITerminalChildProcess, ITerminalLaunchError, ITerminalDimensionsOverride } from 'vs/workbench/contrib/terminal/common/terminal'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; @@ -21,6 +20,7 @@ import { localize } from 'vs/nls'; import { NotSupportedError } from 'vs/base/common/errors'; import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { generateUuid } from 'vs/base/common/uuid'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, IDisposable { @@ -47,63 +47,8 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID export const IExtHostTerminalService = createDecorator('IExtHostTerminalService'); -export class BaseExtHostTerminal { - public _id: number | undefined; - protected _idPromise: Promise; - private _idPromiseComplete: ((value: number) => any) | undefined; +export class ExtHostTerminal implements vscode.Terminal { private _disposed: boolean = false; - private _queuedRequests: ApiRequest[] = []; - - constructor( - protected _proxy: MainThreadTerminalServiceShape, - id?: number - ) { - this._idPromise = new Promise(c => { - if (id !== undefined) { - this._id = id; - c(id); - } else { - this._idPromiseComplete = c; - } - }); - } - - public dispose(): void { - if (!this._disposed) { - this._disposed = true; - this._queueApiRequest(this._proxy.$dispose, []); - } - } - - protected _checkDisposed() { - if (this._disposed) { - throw new Error('Terminal has already been disposed'); - } - } - - protected _queueApiRequest(callback: (...args: any[]) => void, args: any[]): void { - const request: ApiRequest = new ApiRequest(callback, args); - if (!this._id) { - this._queuedRequests.push(request); - return; - } - request.run(this._proxy, this._id); - } - - public _runQueuedRequests(id: number): void { - this._id = id; - if (this._idPromiseComplete) { - this._idPromiseComplete(id); - this._idPromiseComplete = undefined; - } - this._queuedRequests.forEach((r) => { - r.run(this._proxy, id); - }); - this._queuedRequests.length = 0; - } -} - -export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Terminal { private _pidPromise: Promise; private _cols: number | undefined; private _pidPromiseComplete: ((value: number | undefined) => any) | undefined; @@ -113,12 +58,11 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi public isOpen: boolean = false; constructor( - proxy: MainThreadTerminalServiceShape, + private _proxy: MainThreadTerminalServiceShape, + public _id: TerminalIdentifier, private readonly _creationOptions: vscode.TerminalOptions | vscode.ExtensionTerminalOptions, private _name?: string, - id?: number ) { - super(proxy, id); this._creationOptions = Object.freeze(this._creationOptions); this._pidPromise = new Promise(c => this._pidPromiseComplete = c); } @@ -133,16 +77,35 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi hideFromUser?: boolean, isFeatureTerminal?: boolean ): Promise { - const result = await this._proxy.$createTerminal({ name: this._name, shellPath, shellArgs, cwd, env, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal }); - this._name = result.name; - this._runQueuedRequests(result.id); + if (typeof this._id !== 'string') { + throw new Error('Terminal has already been created'); + } + await this._proxy.$createTerminal(this._id, { name: this._name, shellPath, shellArgs, cwd, env, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal }); } public async createExtensionTerminal(): Promise { - const result = await this._proxy.$createTerminal({ name: this._name, isExtensionTerminal: true }); - this._name = result.name; - this._runQueuedRequests(result.id); - return result.id; + if (typeof this._id !== 'string') { + throw new Error('Terminal has already been created'); + } + await this._proxy.$createTerminal(this._id, { name: this._name, isExtensionTerminal: true }); + // At this point, the id has been set via `$acceptTerminalOpened` + if (typeof this._id === 'string') { + throw new Error('Terminal creation failed'); + } + return this._id; + } + + public dispose(): void { + if (!this._disposed) { + this._disposed = true; + this._proxy.$dispose(this._id); + } + } + + private _checkDisposed() { + if (this._disposed) { + throw new Error('Terminal has already been disposed'); + } } public get name(): string { @@ -194,17 +157,17 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi public sendText(text: string, addNewLine: boolean = true): void { this._checkDisposed(); - this._queueApiRequest(this._proxy.$sendText, [text, addNewLine]); + this._proxy.$sendText(this._id, text, addNewLine); } public show(preserveFocus: boolean): void { this._checkDisposed(); - this._queueApiRequest(this._proxy.$show, [preserveFocus]); + this._proxy.$show(this._id, preserveFocus); } public hide(): void { this._checkDisposed(); - this._queueApiRequest(this._proxy.$hide, []); + this._proxy.$hide(this._id); } public _setProcessId(processId: number | undefined): void { @@ -223,20 +186,6 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi } } -class ApiRequest { - private _callback: (...args: any[]) => void; - private _args: any[]; - - constructor(callback: (...args: any[]) => void, args: any[]) { - this._callback = callback; - this._args = args; - } - - public run(proxy: MainThreadTerminalServiceShape, id: number) { - this._callback.apply(proxy, [id].concat(this._args)); - } -} - export class ExtHostPseudoterminal implements ITerminalChildProcess { private readonly _onProcessData = new Emitter(); public readonly onProcessData: Event = this._onProcessData.event; @@ -271,6 +220,11 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { } } + acknowledgeDataEvent(charCount: number): void { + // No-op, flow control is not supported in extension owned terminals. If this is ever + // implemented it will need new pause and resume VS Code APIs. + } + getInitialCwd(): Promise { return Promise.resolve(''); } @@ -296,6 +250,11 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { } this._pty.open(initialDimensions ? initialDimensions : undefined); + + if (this._pty.setDimensions && initialDimensions) { + this._pty.setDimensions(initialDimensions); + } + this._onProcessReady.fire({ pid: -1, cwd: '' }); } } @@ -370,7 +329,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public abstract $acceptWorkspacePermissionsChanged(isAllowed: boolean): void; public createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal { - const terminal = new ExtHostTerminal(this._proxy, options, options.name); + const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); const p = new ExtHostPseudoterminal(options.pty); terminal.createExtensionTerminal().then(id => { const disposable = this._setupExtHostProcessListeners(id, p); @@ -381,7 +340,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void { - const terminal = this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (!terminal) { throw new Error(`Cannot resolve terminal with id ${id} for virtual process`); } @@ -399,7 +358,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } return; } - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (terminal) { this._activeTerminal = terminal; if (original !== this._activeTerminal) { @@ -409,14 +368,14 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public async $acceptTerminalProcessData(id: number, data: string): Promise { - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (terminal) { this._onDidWriteTerminalData.fire({ terminal, data }); } } public async $acceptTerminalDimensions(id: number, cols: number, rows: number): Promise { - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (terminal) { if (terminal.setDimensions(cols, rows)) { this._onDidChangeTerminalDimensions.fire({ @@ -428,23 +387,19 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } public async $acceptTerminalMaximumDimensions(id: number, cols: number, rows: number): Promise { - await this._getTerminalByIdEventually(id); - // Extension pty terminal only - when virtual process resize fires it means that the // terminal's maximum dimensions changed this._terminalProcesses.get(id)?.resize(cols, rows); } public async $acceptTerminalTitleChange(id: number, name: string): Promise { - await this._getTerminalByIdEventually(id); - const extHostTerminal = this._getTerminalObjectById(this.terminals, id); - if (extHostTerminal) { - extHostTerminal.name = name; + const terminal = this._getTerminalById(id); + if (terminal) { + terminal.name = name; } } public async $acceptTerminalClosed(id: number, exitCode: number | undefined): Promise { - await this._getTerminalByIdEventually(id); const index = this._getTerminalObjectIndexById(this.terminals, id); if (index !== null) { const terminal = this._terminals.splice(index, 1)[0]; @@ -453,13 +408,17 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } } - public $acceptTerminalOpened(id: number, name: string, shellLaunchConfigDto: IShellLaunchConfigDto): void { - const index = this._getTerminalObjectIndexById(this._terminals, id); - if (index !== null) { - // The terminal has already been created (via createTerminal*), only fire the event - this._onDidOpenTerminal.fire(this.terminals[index]); - this.terminals[index].isOpen = true; - return; + public $acceptTerminalOpened(id: number, extHostTerminalId: string | undefined, name: string, shellLaunchConfigDto: IShellLaunchConfigDto): void { + if (extHostTerminalId) { + // Resolve with the renderer generated id + const index = this._getTerminalObjectIndexById(this._terminals, extHostTerminalId); + if (index !== null) { + // The terminal has already been created (via createTerminal*), only fire the event + this.terminals[index]._id = id; + this._onDidOpenTerminal.fire(this.terminals[index]); + this.terminals[index].isOpen = true; + return; + } } const creationOptions: vscode.TerminalOptions = { @@ -470,14 +429,14 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I env: shellLaunchConfigDto.env, hideFromUser: shellLaunchConfigDto.hideFromUser }; - const terminal = new ExtHostTerminal(this._proxy, creationOptions, name, id); + const terminal = new ExtHostTerminal(this._proxy, id, creationOptions, name); this._terminals.push(terminal); this._onDidOpenTerminal.fire(terminal); terminal.isOpen = true; } public async $acceptTerminalProcessId(id: number, processId: number): Promise { - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (terminal) { terminal._setProcessId(processId); } @@ -486,7 +445,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public async $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise { // Make sure the ExtHostTerminal exists so onDidOpenTerminal has fired before we call // Pseudoterminal.start - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (!terminal) { return { message: localize('launchFail.idMissingOnExtHost', "Could not find the terminal with id {0} on the extension host", id) }; } @@ -539,6 +498,10 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I return disposables; } + public $acceptProcessAckDataEvent(id: number, charCount: number): void { + this._terminalProcesses.get(id)?.acknowledgeDataEvent(charCount); + } + public $acceptProcessInput(id: number, data: string): void { this._terminalProcesses.get(id)?.input(data); } @@ -670,32 +633,6 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I this._proxy.$sendProcessExit(id, exitCode); } - // TODO: This could be improved by using a single promise and resolve it when the terminal is ready - private _getTerminalByIdEventually(id: number, retries: number = 5): Promise { - if (!this._getTerminalPromises[id]) { - this._getTerminalPromises[id] = this._createGetTerminalPromise(id, retries); - } - return this._getTerminalPromises[id]; - } - - private _createGetTerminalPromise(id: number, retries: number = 5): Promise { - return new Promise(c => { - if (retries === 0) { - c(undefined); - return; - } - - const terminal = this._getTerminalById(id); - if (terminal) { - c(terminal); - } else { - // This should only be needed immediately after createTerminalRenderer is called as - // the ExtHostTerminal has not yet been iniitalized - timeout(EXT_HOST_CREATION_DELAY * 2).then(() => c(this._createGetTerminalPromise(id, retries - 1))); - } - }); - } - private _getTerminalById(id: number): ExtHostTerminal | null { return this._getTerminalObjectById(this._terminals, id); } @@ -705,7 +642,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I return index !== null ? array[index] : null; } - private _getTerminalObjectIndexById(array: T[], id: number): number | null { + private _getTerminalObjectIndexById(array: T[], id: TerminalIdentifier): number | null { let index: number | null = null; array.some((item, i) => { const thisId = item._id; diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 02fc32cbf..fb3084566 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -6,7 +6,6 @@ import { mapFind } from 'vs/base/common/arrays'; import { disposableTimeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { throttle } from 'vs/base/common/decorators'; import { Emitter } from 'vs/base/common/event'; import { once } from 'vs/base/common/functional'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; @@ -14,25 +13,35 @@ import { isDefined } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { ExtHostTestingResource, ExtHostTestingShape, MainContext, MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { TestItem } from 'vs/workbench/api/common/extHostTypeConverters'; -import { Disposable, RequiredTestItem } from 'vs/workbench/api/common/extHostTypes'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; +import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, InternalTestItemWithChildren, InternalTestResults, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import type * as vscode from 'vscode'; const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`; export class ExtHostTesting implements ExtHostTestingShape { + private readonly resultsChangedEmitter = new Emitter(); private readonly providers = new Map(); private readonly proxy: MainThreadTestingShape; private readonly ownedTests = new OwnedTestCollection(); - private readonly testSubscriptions = new Map(); + private readonly testSubscriptions = new Map void; + }>(); private workspaceObservers: WorkspaceFolderTestObserverFactory; private textDocumentObservers: TextDocumentTestObserverFactory; + public onLastResultsChanged = this.resultsChangedEmitter.event; + public lastResults?: vscode.TestResults; + constructor(@IExtHostRpcService rpc: IExtHostRpcService, @IExtHostDocumentsAndEditors private readonly documents: IExtHostDocumentsAndEditors, @IExtHostWorkspace private readonly workspace: IExtHostWorkspace) { this.proxy = rpc.getProxy(MainContext.MainThreadTesting); this.workspaceObservers = new WorkspaceFolderTestObserverFactory(this.proxy); @@ -47,6 +56,14 @@ export class ExtHostTesting implements ExtHostTestingShape { this.providers.set(providerId, provider); this.proxy.$registerTestProvider(providerId); + // give the ext a moment to register things rather than synchronously invoking within activate() + const toSubscribe = [...this.testSubscriptions.keys()]; + setTimeout(() => { + for (const subscription of toSubscribe) { + this.testSubscriptions.get(subscription)?.subscribeFn(providerId, provider); + } + }, 0); + return new Disposable(() => { this.providers.delete(providerId); this.proxy.$unregisterTestProvider(providerId); @@ -70,7 +87,7 @@ export class ExtHostTesting implements ExtHostTestingShape { /** * Implements vscode.test.runTests */ - public async runTests(req: vscode.TestRunOptions) { + public async runTests(req: vscode.TestRunOptions, token = CancellationToken.None) { await this.proxy.$runTests({ tests: req.tests // Find workspace items first, then owned tests, then document tests. @@ -82,14 +99,27 @@ export class ExtHostTesting implements ExtHostTestingShape { .filter(isDefined) .map(item => ({ providerId: item.providerId, testId: item.id })), debug: req.debug - }); + }, token); + } + + + /** + * Updates test results shown to extensions. + * @override + */ + public $publishTestResults(results: InternalTestResults): void { + const convert = (item: InternalTestItemWithChildren): vscode.RequiredTestItem => + ({ ...TestItem.toShallow(item.item), children: item.children.map(convert) }); + + this.lastResults = { tests: results.tests.map(convert) }; + this.resultsChangedEmitter.fire(); } /** * Handles a request to read tests for a file, or workspace. * @override */ - public $subscribeToTests(resource: ExtHostTestingResource, uriComponents: UriComponents) { + public async $subscribeToTests(resource: ExtHostTestingResource, uriComponents: UriComponents) { const uri = URI.revive(uriComponents); const subscriptionKey = getTestSubscriptionKey(resource, uri); if (this.testSubscriptions.has(subscriptionKey)) { @@ -98,12 +128,29 @@ export class ExtHostTesting implements ExtHostTestingShape { let method: undefined | ((p: vscode.TestProvider) => vscode.TestHierarchy | undefined); if (resource === ExtHostTestingResource.TextDocument) { - const document = this.documents.getDocument(uri); + let document = this.documents.getDocument(uri); + + // we can ask to subscribe to tests before the documents are populated in + // the extension host. Try to wait. + if (!document) { + const store = new DisposableStore(); + document = await new Promise(resolve => { + store.add(disposableTimeout(() => resolve(undefined), 5000)); + store.add(this.documents.onDidAddDocuments(e => { + const data = e.find(data => data.document.uri.toString() === uri.toString()); + if (data) { resolve(data); } + })); + }).finally(() => store.dispose()); + } + if (document) { - method = p => p.createDocumentTestHierarchy?.(document.document); + const folder = await this.workspace.getWorkspaceFolder2(uri, false); + method = p => p.createDocumentTestHierarchy + ? p.createDocumentTestHierarchy(document!.document) + : this.createDefaultDocumentTestHierarchy(p, document!.document, folder); } } else { - const folder = this.workspace.getWorkspaceFolder(uri, false); + const folder = await this.workspace.getWorkspaceFolder2(uri, false); if (folder) { method = p => p.createWorkspaceTestHierarchy?.(folder); } @@ -113,24 +160,34 @@ export class ExtHostTesting implements ExtHostTestingShape { return; } - const disposable = new DisposableStore(); - const collection = disposable.add(this.ownedTests.createForHierarchy(diff => this.proxy.$publishDiff(resource, uriComponents, diff))); - for (const [id, provider] of this.providers) { + const subscribeFn = (id: string, provider: vscode.TestProvider) => { try { - const hierarchy = method(provider); + const hierarchy = method!(provider); if (!hierarchy) { - continue; + return; } + collection.pushDiff([TestDiffOpType.DeltaDiscoverComplete, 1]); disposable.add(hierarchy); collection.addRoot(hierarchy.root, id); + Promise.resolve(hierarchy.discoveredInitialTests).then(() => collection.pushDiff([TestDiffOpType.DeltaDiscoverComplete, -1])); hierarchy.onDidChangeTest(e => collection.onItemChange(e, id)); } catch (e) { console.error(e); } + }; + + const disposable = new DisposableStore(); + const collection = disposable.add(this.ownedTests.createForHierarchy(diff => this.proxy.$publishDiff(resource, uriComponents, diff))); + for (const [id, provider] of this.providers) { + subscribeFn(id, provider); } - this.testSubscriptions.set(subscriptionKey, { store: disposable, collection }); + // note: we don't increment the root count initially -- this is done by the + // main thread, incrementing once per extension host. We just push the + // diff to signal that roots have been discovered. + collection.pushDiff([TestDiffOpType.DeltaRootsComplete, -1]); + this.testSubscriptions.set(subscriptionKey, { store: disposable, collection, subscribeFn }); } /** @@ -162,217 +219,193 @@ export class ExtHostTesting implements ExtHostTestingShape { * providers to be run. * @override */ - public async $runTestsForProvider(req: RunTestForProviderRequest): Promise { + public async $runTestsForProvider(req: RunTestForProviderRequest, cancellation: CancellationToken): Promise { const provider = this.providers.get(req.providerId); if (!provider || !provider.runTests) { return EMPTY_TEST_RESULT; } - const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual).filter(isDefined); + const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual) + .filter(isDefined) + // Only send the actual TestItem's to the user to run. + .map(t => t instanceof TestItemFilteredWrapper ? t.actual : t); if (!tests.length) { return EMPTY_TEST_RESULT; } - await provider.runTests({ tests, debug: req.debug }, CancellationToken.None); - return EMPTY_TEST_RESULT; - } -} - -const keyMap: { [K in keyof Omit]: null } = { - label: null, - location: null, - state: null, - debuggable: null, - description: null, - runnable: null -}; - -const simpleProps = Object.keys(keyMap) as ReadonlyArray; - -const itemEqualityComparator = (a: vscode.TestItem) => { - const values: unknown[] = []; - for (const prop of simpleProps) { - values.push(a[prop]); - } - - return (b: vscode.TestItem) => { - for (let i = 0; i < simpleProps.length; i++) { - if (values[i] !== b[simpleProps[i]]) { - return false; + try { + await provider.runTests({ tests, debug: req.debug }, cancellation); + for (const { collection } of this.testSubscriptions.values()) { + collection.flushDiff(); // ensure all states are updated } + + return EMPTY_TEST_RESULT; + } catch (e) { + console.error(e); // so it appears to attached debuggers + throw e; + } + } + + public $lookupTest(req: TestIdWithProvider): Promise { + const owned = this.ownedTests.getTestById(req.testId); + if (!owned) { + return Promise.resolve(undefined); } - return true; - }; -}; - -/** - * @private - */ -export interface OwnedCollectionTestItem extends InternalTestItem { - actual: vscode.TestItem; - previousChildren: Set; - previousEquals: (v: vscode.TestItem) => boolean; -} - -/** - * @private - */ -export class OwnedTestCollection { - protected readonly testIdToInternal = new Map(); - - /** - * Gets test information by ID, if it was defined and still exists in this - * extension host. - */ - public getTestById(id: string) { - return this.testIdToInternal.get(id); + const { actual, previousChildren, previousEquals, ...item } = owned; + return Promise.resolve(item); } - /** - * Creates a new test collection for a specific hierarchy for a workspace - * or document observation. - */ - public createForHierarchy(publishDiff: (diff: TestsDiff) => void = () => undefined) { - return new SingleUseTestCollection(this.testIdToInternal, publishDiff); - } -} - -/** - * Maintains tests created and registered for a single set of hierarchies - * for a workspace or document. - * @private - */ -export class SingleUseTestCollection implements IDisposable { - protected readonly testItemToInternal = new Map(); - protected diff: TestsDiff = []; - private disposed = false; - - constructor(private readonly testIdToInternal: Map, private readonly publishDiff: (diff: TestsDiff) => void) { } - - /** - * Adds a new root node to the collection. - */ - public addRoot(item: vscode.TestItem, providerId: string) { - this.addItem(item, providerId, null); - this.throttleSendDiff(); - } - - /** - * Gets test information by its reference, if it was defined and still exists - * in this extension host. - */ - public getTestByReference(item: vscode.TestItem) { - return this.testItemToInternal.get(item); - } - - /** - * Should be called when an item change is fired on the test provider. - */ - public onItemChange(item: vscode.TestItem, providerId: string) { - const existing = this.testItemToInternal.get(item); - if (!existing) { - if (!this.disposed) { - console.warn(`Received a TestProvider.onDidChangeTest for a test that wasn't seen before as a child.`); - } + private createDefaultDocumentTestHierarchy(provider: vscode.TestProvider, document: vscode.TextDocument, folder: vscode.WorkspaceFolder | undefined): vscode.TestHierarchy | undefined { + if (!folder) { return; } - this.addItem(item, providerId, existing.parent); - this.throttleSendDiff(); - } - - /** - * Gets a diff of all changes that have been made, and clears the diff queue. - */ - public collectDiff() { - const diff = this.diff; - this.diff = []; - return diff; - } - - public dispose() { - for (const item of this.testItemToInternal.values()) { - this.testIdToInternal.delete(item.id); + const workspaceHierarchy = provider.createWorkspaceTestHierarchy?.(folder); + if (!workspaceHierarchy) { + return; } - this.testIdToInternal.clear(); - this.diff = []; - this.disposed = true; - } + const onDidChangeTest = new Emitter(); + workspaceHierarchy.onDidChangeTest(node => { + const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(node, document); + const previouslySeen = wrapper.hasNodeMatchingFilter; - protected getId(): string { - return generateUuid(); - } + if (previouslySeen) { + // reset cache and get whether you can currently see the TestItem. + wrapper.reset(); + const currentlySeen = wrapper.hasNodeMatchingFilter; - private addItem(actual: vscode.TestItem, providerId: string, parent: string | null) { - let internal = this.testItemToInternal.get(actual); - if (!internal) { - internal = { - actual, - id: this.getId(), - parent, - item: TestItem.from(actual), - providerId, - previousChildren: new Set(), - previousEquals: itemEqualityComparator(actual), - }; + if (currentlySeen) { + onDidChangeTest.fire(wrapper); + return; + } - this.testItemToInternal.set(actual, internal); - this.testIdToInternal.set(internal.id, internal); - this.diff.push([TestDiffOpType.Add, { id: internal.id, parent, providerId, item: internal.item }]); - } else if (!internal.previousEquals(actual)) { - internal.item = TestItem.from(actual); - internal.previousEquals = itemEqualityComparator(actual); - this.diff.push([TestDiffOpType.Update, { id: internal.id, parent, providerId, item: internal.item }]); - } - - // If there are children, track which ones are deleted - // and recursively and/update them. - if (actual.children) { - const deletedChildren = internal.previousChildren; - const currentChildren = new Set(); - for (const child of actual.children) { - const c = this.addItem(child, providerId, internal.id); - deletedChildren.delete(c.id); - currentChildren.add(c.id); + // Fire the event to say that the current visible parent has changed. + onDidChangeTest.fire(wrapper.visibleParent); + return; } - for (const child of deletedChildren) { - this.removeItembyId(child); + const previousParent = wrapper.visibleParent; + wrapper.reset(); + const currentlySeen = wrapper.hasNodeMatchingFilter; + + // It wasn't previously seen and isn't currently seen so + // nothing has actually changed. + if (!currentlySeen) { + return; } - internal.previousChildren = currentChildren; - } + // The test is now visible so we need to refresh the cache + // of the previous visible parent and fire that it has changed. + previousParent.reset(); + onDidChangeTest.fire(previousParent); + }); + return { + root: TestItemFilteredWrapper.getWrapperForTestItem(workspaceHierarchy.root, document), + dispose: () => { + onDidChangeTest.dispose(); + TestItemFilteredWrapper.removeFilter(document); + }, + onDidChangeTest: onDidChangeTest.event + }; + } +} - return internal; +/* + * A class which wraps a vscode.TestItem that provides the ability to filter a TestItem's children + * to only the children that are located in a certain vscode.Uri. + */ +export class TestItemFilteredWrapper implements vscode.TestItem { + private static wrapperMap = new WeakMap>(); + public static removeFilter(document: vscode.TextDocument): void { + this.wrapperMap.delete(document); } - private removeItembyId(id: string) { - this.diff.push([TestDiffOpType.Remove, id]); - - const queue = [this.testIdToInternal.get(id)]; - while (queue.length) { - const item = queue.pop(); - if (!item) { - continue; - } - - this.testIdToInternal.delete(item.id); - this.testItemToInternal.delete(item.actual); - for (const child of item.previousChildren) { - queue.push(this.testIdToInternal.get(child)); - } + // Wraps the TestItem specified in a TestItemFilteredWrapper and pulls from a cache if it already exists. + public static getWrapperForTestItem(item: vscode.TestItem, filterDocument: vscode.TextDocument, parent?: TestItemFilteredWrapper): TestItemFilteredWrapper { + let innerMap = this.wrapperMap.get(filterDocument); + if (innerMap?.has(item)) { + return innerMap.get(item)!; } + + if (!innerMap) { + innerMap = new WeakMap(); + this.wrapperMap.set(filterDocument, innerMap); + + } + + const w = new TestItemFilteredWrapper(item, filterDocument, parent); + innerMap.set(item, w); + return w; } - @throttle(200) - protected throttleSendDiff() { - const diff = this.collectDiff(); - if (diff.length) { - this.publishDiff(diff); + public get label() { + return this.actual.label; + } + + public get debuggable() { + return this.actual.debuggable; + } + + public get description() { + return this.actual.description; + } + + public get location() { + return this.actual.location; + } + + public get runnable() { + return this.actual.runnable; + } + + public get state() { + return this.actual.state; + } + + public get children() { + // We only want children that match the filter. + return this.getWrappedChildren().filter(child => child.hasNodeMatchingFilter); + } + + public get visibleParent(): TestItemFilteredWrapper { + return this.hasNodeMatchingFilter ? this : this.parent!.visibleParent; + } + + private matchesFilter: boolean | undefined; + + // Determines if the TestItem matches the filter. This would be true if: + // 1. We don't have a parent (because the root is the workspace root node) + // 2. The URI of the current node matches the filter URI + // 3. Some child of the current node matches the filter URI + public get hasNodeMatchingFilter(): boolean { + if (this.matchesFilter === undefined) { + this.matchesFilter = !this.parent + || this.actual.location?.uri.toString() === this.filterDocument.uri.toString() + || this.getWrappedChildren().some(child => child.hasNodeMatchingFilter); } + + return this.matchesFilter; + } + + // Reset the cache of whether or not you can see a node from a particular node + // up to it's visible parent. + public reset(): void { + if (this !== this.visibleParent) { + this.parent?.reset(); + } + this.matchesFilter = undefined; + } + + + private constructor(public readonly actual: vscode.TestItem, private filterDocument: vscode.TextDocument, private readonly parent?: TestItemFilteredWrapper) { + this.getWrappedChildren(); + } + + private getWrappedChildren() { + return this.actual.children?.map(t => TestItemFilteredWrapper.getWrapperForTestItem(t, this.filterDocument, this)) || []; } } @@ -382,7 +415,7 @@ export class SingleUseTestCollection implements IDisposable { interface MirroredCollectionTestItem extends IncrementalTestCollectionItem { revived: vscode.TestItem; depth: number; - wrapped?: vscode.TestItem; + wrapped?: vscode.RequiredTestItem; } class MirroredChangeCollector extends IncrementalChangeCollector { @@ -411,7 +444,7 @@ class MirroredChangeCollector extends IncrementalChangeCollector): vscode.TestItem[] { - let output: vscode.TestItem[] = []; + public getAllAsTestItem(itemIds: Iterable): vscode.RequiredTestItem[] { + let output: vscode.RequiredTestItem[] = []; for (const itemId of itemIds) { const item = this.items.get(itemId); if (item) { @@ -577,7 +610,7 @@ export class MirroredTestCollection extends AbstractIncrementalTestCollection { const MirroredItemId = Symbol('MirroredItemId'); -class ExtHostTestItem implements vscode.TestItem, RequiredTestItem { +class TestItemFromMirror implements vscode.RequiredTestItem { readonly #internal: MirroredCollectionTestItem; readonly #collection: MirroredTestCollection; + public get id() { return this.#internal.revived.id!; } public get label() { return this.#internal.revived.label; } public get description() { return this.#internal.revived.description; } public get state() { return this.#internal.revived.state; } @@ -627,14 +661,18 @@ class ExtHostTestItem implements vscode.TestItem, RequiredTestItem { } public toJSON() { - const serialized: RequiredTestItem = { + const serialized: vscode.RequiredTestItem & TestIdWithProvider = { + id: this.id, label: this.label, description: this.description, state: this.state, location: this.location, runnable: this.runnable, debuggable: this.debuggable, - children: this.children.map(c => (c as ExtHostTestItem).toJSON()), + children: this.children.map(c => (c as TestItemFromMirror).toJSON()), + + providerId: this.#internal.providerId, + testId: this.#internal.id, }; return serialized; @@ -655,6 +693,7 @@ abstract class AbstractTestObserverFactory { const resourceKey = resourceUri.toString(); const resource = this.resources.get(resourceKey) ?? this.createObserverData(resourceUri); + resource.pendingDeletion?.dispose(); resource.observers++; return { @@ -778,16 +817,9 @@ class TextDocumentTestObserverFactory extends AbstractTestObserverFactory { const uriString = resourceUri.toString(); this.diffListeners.set(uriString, onDiff); - const disposeListener = this.documents.onDidRemoveDocuments(evt => { - if (evt.some(delta => delta.document.uri.toString() === uriString)) { - this.unlisten(resourceUri); - } - }); - this.proxy.$subscribeToDiffs(ExtHostTestingResource.TextDocument, resourceUri); return new Disposable(() => { this.proxy.$unsubscribeFromDiffs(ExtHostTestingResource.TextDocument, resourceUri); - disposeListener.dispose(); this.diffListeners.delete(uriString); }); } diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 63d307018..ca1914d2e 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -21,6 +21,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { MarkdownString } from 'vs/workbench/api/common/extHostTypeConverters'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Command } from 'vs/editor/common/modes'; type TreeItemHandle = string; @@ -132,12 +133,12 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return treeView.hasResolve; } - $resolve(treeViewId: string, treeItemHandle: string): Promise { + $resolve(treeViewId: string, treeItemHandle: string, token: vscode.CancellationToken): Promise { const treeView = this.treeViews.get(treeViewId); if (!treeView) { throw new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId)); } - return treeView.resolveTreeItem(treeItemHandle); + return treeView.resolveTreeItem(treeItemHandle, token); } $setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void { @@ -184,6 +185,7 @@ interface TreeNode extends IDisposable { extensionItem: vscode.TreeItem; parent: TreeNode | Root; children?: TreeNode[]; + disposableStore: DisposableStore; } class ExtHostTreeView extends Disposable { @@ -370,7 +372,7 @@ class ExtHostTreeView extends Disposable { return !!this.dataProvider.resolveTreeItem; } - async resolveTreeItem(treeItemHandle: string): Promise { + async resolveTreeItem(treeItemHandle: string, token: vscode.CancellationToken): Promise { if (!this.dataProvider.resolveTreeItem) { return; } @@ -378,9 +380,10 @@ class ExtHostTreeView extends Disposable { if (element) { const node = this.nodes.get(element); if (node) { - const resolve = await this.dataProvider.resolveTreeItem(node.extensionItem, element) ?? node.extensionItem; - // Resolvable elements. Currently only tooltip. + const resolve = await this.dataProvider.resolveTreeItem(node.extensionItem, element, token) ?? node.extensionItem; + // Resolvable elements. Currently only tooltip and command. node.item.tooltip = this.getTooltip(resolve.tooltip); + node.item.command = this.getCommand(node.disposableStore, resolve.command); return node.item; } } @@ -573,8 +576,12 @@ class ExtHostTreeView extends Disposable { return tooltip; } + private getCommand(disposable: DisposableStore, command?: vscode.Command): Command | undefined { + return command ? this.commands.toInternal(command, disposable) : undefined; + } + private createTreeNode(element: T, extensionTreeItem: vscode.TreeItem, parent: TreeNode | Root): TreeNode { - const disposable = new DisposableStore(); + const disposableStore = new DisposableStore(); const handle = this.createHandle(element, extensionTreeItem, parent); const icon = this.getLightIconPath(extensionTreeItem); const item: ITreeItem = { @@ -584,7 +591,7 @@ class ExtHostTreeView extends Disposable { description: extensionTreeItem.description, resourceUri: extensionTreeItem.resourceUri, tooltip: this.getTooltip(extensionTreeItem.tooltip), - command: extensionTreeItem.command ? this.commands.toInternal(extensionTreeItem.command, disposable) : undefined, + command: this.getCommand(disposableStore, extensionTreeItem.command), contextValue: extensionTreeItem.contextValue, icon, iconDark: this.getDarkIconPath(extensionTreeItem) || icon, @@ -598,7 +605,8 @@ class ExtHostTreeView extends Disposable { extensionItem: extensionTreeItem, parent, children: undefined, - dispose(): void { disposable.dispose(); } + disposableStore, + dispose(): void { disposableStore.dispose(); } }; } diff --git a/src/vs/workbench/api/common/extHostTunnelService.ts b/src/vs/workbench/api/common/extHostTunnelService.ts index 29a50ae27..721cbaba4 100644 --- a/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/src/vs/workbench/api/common/extHostTunnelService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtHostTunnelServiceShape, MainContext, MainThreadTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import * as vscode from 'vscode'; import { RemoteTunnel, TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel'; @@ -11,18 +11,27 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; export interface TunnelDto { remoteAddress: { port: number, host: string }; localAddress: { port: number, host: string } | string; + public: boolean; } export namespace TunnelDto { export function fromApiTunnel(tunnel: vscode.Tunnel): TunnelDto { - return { remoteAddress: tunnel.remoteAddress, localAddress: tunnel.localAddress }; + return { remoteAddress: tunnel.remoteAddress, localAddress: tunnel.localAddress, public: !!tunnel.public }; } export function fromServiceTunnel(tunnel: RemoteTunnel): TunnelDto { - return { remoteAddress: { host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }, localAddress: tunnel.localAddress }; + return { + remoteAddress: { + host: tunnel.tunnelRemoteHost, + port: tunnel.tunnelRemotePort + }, + localAddress: tunnel.localAddress, + public: tunnel.public + }; } } @@ -44,12 +53,13 @@ export const IExtHostTunnelService = createDecorator('IEx export class ExtHostTunnelService implements IExtHostTunnelService { declare readonly _serviceBrand: undefined; onDidChangeTunnels: vscode.Event = (new Emitter()).event; - private readonly _proxy: MainThreadTunnelServiceShape; constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, ) { - this._proxy = extHostRpc.getProxy(MainContext.MainThreadTunnelService); + } + async $applyCandidateFilter(candidates: CandidatePort[]): Promise { + return candidates; } async openTunnel(extension: IExtensionDescription, forward: TunnelOptions): Promise { @@ -59,10 +69,10 @@ export class ExtHostTunnelService implements IExtHostTunnelService { return []; } async setTunnelExtensionFunctions(provider: vscode.RemoteAuthorityResolver | undefined): Promise { - await this._proxy.$tunnelServiceReady(); return { dispose: () => { } }; } - $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined { return undefined; } + async $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise { return undefined; } async $closeTunnel(remote: { host: string, port: number }): Promise { } async $onDidTunnelsChange(): Promise { } + async $registerCandidateFinder(): Promise { } } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index d80dfae4b..d1512e59b 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1015,6 +1015,29 @@ export namespace SignatureHelp { } } +export namespace InlineHint { + + export function from(hint: vscode.InlineHint): modes.InlineHint { + return { + text: hint.text, + range: Range.from(hint.range), + description: hint.description && MarkdownString.fromStrict(hint.description), + whitespaceBefore: hint.whitespaceBefore, + whitespaceAfter: hint.whitespaceAfter + }; + } + + export function to(hint: modes.InlineHint): vscode.InlineHint { + return new types.InlineHint( + hint.text, + Range.to(hint.range), + htmlContent.isMarkdownString(hint.description) ? MarkdownString.to(hint.description) : hint.description, + hint.whitespaceBefore, + hint.whitespaceAfter + ); + } +} + export namespace DocumentLink { export function from(link: vscode.DocumentLink): modes.ILink { @@ -1389,19 +1412,21 @@ export namespace TestState { export namespace TestItem { - export function from(item: vscode.TestItem): ITestItem { + export function from(item: vscode.TestItem, parentExtId?: string): ITestItem { return { + extId: item.id ?? (parentExtId ? `${parentExtId}\0${item.label}` : item.label), label: item.label, location: item.location ? location.from(item.location) : undefined, - debuggable: item.debuggable, + debuggable: item.debuggable ?? false, description: item.description, - runnable: item.runnable, + runnable: item.runnable ?? true, state: TestState.from(item.state), }; } - export function to(item: ITestItem): vscode.TestItem { + export function toShallow(item: ITestItem): Omit { return { + id: item.extId, label: item.label, location: item.location && location.to({ range: item.location.range, diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 069c56583..5d1bb34cb 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -13,7 +13,7 @@ import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files'; import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { addIdToOutput, CellEditType, ICellEditOperation, IDisplayOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { addIdToOutput, CellEditType, ICellEditOperation, IDisplayOutput, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import type * as vscode from 'vscode'; function es5ClassCompat(target: Function): any { @@ -638,7 +638,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { // --- notebook replaceNotebookMetadata(uri: URI, value: vscode.NotebookDocumentMetadata, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: FileEditType.Cell, metadata, uri, notebookMetadata: value }); + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.DocumentMetadata, metadata: { ...notebookDocumentMetadataDefaults, ...value } }, notebookMetadata: value }); } replaceNotebookCells(uri: URI, start: number, end: number, cells: vscode.NotebookCellData[], metadata?: vscode.WorkspaceEditEntryMetadata): void { @@ -1373,6 +1373,23 @@ export enum SignatureHelpTriggerKind { ContentChange = 3, } +@es5ClassCompat +export class InlineHint { + text: string; + range: Range; + description?: string | vscode.MarkdownString; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; + + constructor(text: string, range: Range, description?: string | vscode.MarkdownString, whitespaceBefore?: boolean, whitespaceAfter?: boolean) { + this.text = text; + this.range = range; + this.description = description; + this.whitespaceBefore = whitespaceBefore; + this.whitespaceAfter = whitespaceAfter; + } +} + export enum CompletionTriggerKind { Invoke = 0, TriggerCharacter = 1, @@ -2298,9 +2315,6 @@ export class FunctionBreakpoint extends Breakpoint { constructor(functionName: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { super(enabled, condition, hitCondition, logMessage); - if (!functionName) { - throw illegalArgument('functionName'); - } this.functionName = functionName; } } @@ -2858,7 +2872,8 @@ export enum NotebookCellStatusBarAlignment { export enum NotebookEditorRevealType { Default = 0, InCenter = 1, - InCenterIfOutsideViewport = 2 + InCenterIfOutsideViewport = 2, + AtTop = 3 } @@ -2924,11 +2939,12 @@ export class LinkedEditingRanges { //#region Testing export enum TestRunState { Unset = 0, - Running = 1, - Passed = 2, - Failed = 3, - Skipped = 4, - Errored = 5 + Queued = 1, + Running = 2, + Passed = 3, + Failed = 4, + Skipped = 5, + Errored = 6 } export enum TestMessageSeverity { @@ -2963,15 +2979,15 @@ export class TestState { } } -type AllowedUndefined = 'description' | 'location'; - -/** - * Test item without any optional properties. Only some properties are - * permitted to be undefined, but they must still exist. - */ -export type RequiredTestItem = { - [K in keyof Required]: K extends AllowedUndefined ? vscode.TestItem[K] : Required[K] -}; +export type RequiredTestItem = vscode.RequiredTestItem; +export type TestItem = vscode.TestItem; //#endregion + +export enum ExternalUriOpenerPriority { + None = 0, + Option = 1, + Default = 2, + Preferred = 3, +} diff --git a/src/vs/workbench/api/common/extHostUriOpener.ts b/src/vs/workbench/api/common/extHostUriOpener.ts new file mode 100644 index 000000000..81bbc6f4d --- /dev/null +++ b/src/vs/workbench/api/common/extHostUriOpener.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import * as modes from 'vs/editor/common/modes'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import type * as vscode from 'vscode'; +import { ExtHostUriOpenersShape, IMainContext, MainContext, MainThreadUriOpenersShape } from './extHost.protocol'; + + +export class ExtHostUriOpeners implements ExtHostUriOpenersShape { + + private static readonly supportedSchemes = new Set([Schemas.http, Schemas.https]); + + private readonly _proxy: MainThreadUriOpenersShape; + + private readonly _openers = new Map(); + + constructor( + mainContext: IMainContext, + ) { + this._proxy = mainContext.getProxy(MainContext.MainThreadUriOpeners); + } + + registerExternalUriOpener( + extensionId: ExtensionIdentifier, + id: string, + opener: vscode.ExternalUriOpener, + metadata: vscode.ExternalUriOpenerMetadata, + ): vscode.Disposable { + if (this._openers.has(id)) { + throw new Error(`Opener with id '${id}' already registered`); + } + + const invalidScheme = metadata.schemes.find(scheme => !ExtHostUriOpeners.supportedSchemes.has(scheme)); + if (invalidScheme) { + throw new Error(`Scheme '${invalidScheme}' is not supported. Only http and https are currently supported.`); + } + + this._openers.set(id, opener); + this._proxy.$registerUriOpener(id, metadata.schemes, extensionId, metadata.label); + + return toDisposable(() => { + this._openers.delete(id); + this._proxy.$unregisterUriOpener(id); + }); + } + + async $canOpenUri(id: string, uriComponents: UriComponents, token: CancellationToken): Promise { + const opener = this._openers.get(id); + if (!opener) { + throw new Error(`Unknown opener with id: ${id}`); + } + + const uri = URI.revive(uriComponents); + return opener.canOpenExternalUri(uri, token); + } + + async $openUri(id: string, context: { resolvedUri: UriComponents, sourceUri: UriComponents }, token: CancellationToken): Promise { + const opener = this._openers.get(id); + if (!opener) { + throw new Error(`Unknown opener id: '${id}'`); + } + + return opener.openExternalUri(URI.revive(context.resolvedUri), { + sourceUri: URI.revive(context.sourceUri) + }, token); + } +} diff --git a/src/vs/workbench/api/common/extHostWebviewPanels.ts b/src/vs/workbench/api/common/extHostWebviewPanels.ts index 797702b88..dfdd948be 100644 --- a/src/vs/workbench/api/common/extHostWebviewPanels.ts +++ b/src/vs/workbench/api/common/extHostWebviewPanels.ts @@ -287,8 +287,8 @@ export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanel await serializer.deserializeWebviewPanel(revivedPanel, state); } - public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: number, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, webview: ExtHostWebview) { - const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); + public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: vscode.ViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, webview: ExtHostWebview) { + const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, position, options, webview); this._webviewPanels.set(webviewHandle, panel); return panel; } diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index 3e0de07e3..c03c6be54 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -10,7 +10,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { TernarySearchTree } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { Counter } from 'vs/base/common/numbers'; -import { basename, basenameOrAuthority, dirname, isEqual, relativePath } from 'vs/base/common/resources'; +import { basename, basenameOrAuthority, dirname, ExtUri, relativePath } from 'vs/base/common/resources'; import { compare } from 'vs/base/common/strings'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -37,27 +37,27 @@ export interface IExtHostWorkspaceProvider { resolveProxy(url: string): Promise; } -function isFolderEqual(folderA: URI, folderB: URI): boolean { - return isEqual(folderA, folderB); +function isFolderEqual(folderA: URI, folderB: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): boolean { + return new ExtUri(uri => ignorePathCasing(uri, extHostFileSystemInfo)).isEqual(folderA, folderB); } -function compareWorkspaceFolderByUri(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number { - return isFolderEqual(a.uri, b.uri) ? 0 : compare(a.uri.toString(), b.uri.toString()); +function compareWorkspaceFolderByUri(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo): number { + return isFolderEqual(a.uri, b.uri, extHostFileSystemInfo) ? 0 : compare(a.uri.toString(), b.uri.toString()); } -function compareWorkspaceFolderByUriAndNameAndIndex(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number { +function compareWorkspaceFolderByUriAndNameAndIndex(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo): number { if (a.index !== b.index) { return a.index < b.index ? -1 : 1; } - return isFolderEqual(a.uri, b.uri) ? compare(a.name, b.name) : compare(a.uri.toString(), b.uri.toString()); + return isFolderEqual(a.uri, b.uri, extHostFileSystemInfo) ? compare(a.name, b.name) : compare(a.uri.toString(), b.uri.toString()); } -function delta(oldFolders: vscode.WorkspaceFolder[], newFolders: vscode.WorkspaceFolder[], compare: (a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder) => number): { removed: vscode.WorkspaceFolder[], added: vscode.WorkspaceFolder[] } { - const oldSortedFolders = oldFolders.slice(0).sort(compare); - const newSortedFolders = newFolders.slice(0).sort(compare); +function delta(oldFolders: vscode.WorkspaceFolder[], newFolders: vscode.WorkspaceFolder[], compare: (a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo) => number, extHostFileSystemInfo: IExtHostFileSystemInfo): { removed: vscode.WorkspaceFolder[], added: vscode.WorkspaceFolder[] } { + const oldSortedFolders = oldFolders.slice(0).sort((a, b) => compare(a, b, extHostFileSystemInfo)); + const newSortedFolders = newFolders.slice(0).sort((a, b) => compare(a, b, extHostFileSystemInfo)); - return arrayDelta(oldSortedFolders, newSortedFolders, compare); + return arrayDelta(oldSortedFolders, newSortedFolders, (a, b) => compare(a, b, extHostFileSystemInfo)); } function ignorePathCasing(uri: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): boolean { @@ -87,7 +87,7 @@ class ExtHostWorkspaceImpl extends Workspace { if (previousConfirmedWorkspace) { folders.forEach((folderData, index) => { const folderUri = URI.revive(folderData.uri); - const existingFolder = ExtHostWorkspaceImpl._findFolder(previousUnconfirmedWorkspace || previousConfirmedWorkspace, folderUri); + const existingFolder = ExtHostWorkspaceImpl._findFolder(previousUnconfirmedWorkspace || previousConfirmedWorkspace, folderUri, extHostFileSystemInfo); if (existingFolder) { existingFolder.name = folderData.name; @@ -106,15 +106,15 @@ class ExtHostWorkspaceImpl extends Workspace { newWorkspaceFolders.sort((f1, f2) => f1.index < f2.index ? -1 : 1); const workspace = new ExtHostWorkspaceImpl(id, name, newWorkspaceFolders, configuration ? URI.revive(configuration) : null, !!isUntitled, uri => ignorePathCasing(uri, extHostFileSystemInfo)); - const { added, removed } = delta(oldWorkspace ? oldWorkspace.workspaceFolders : [], workspace.workspaceFolders, compareWorkspaceFolderByUri); + const { added, removed } = delta(oldWorkspace ? oldWorkspace.workspaceFolders : [], workspace.workspaceFolders, compareWorkspaceFolderByUri, extHostFileSystemInfo); return { workspace, added, removed }; } - private static _findFolder(workspace: ExtHostWorkspaceImpl, folderUriToFind: URI): MutableWorkspaceFolder | undefined { + private static _findFolder(workspace: ExtHostWorkspaceImpl, folderUriToFind: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): MutableWorkspaceFolder | undefined { for (let i = 0; i < workspace.folders.length; i++) { const folder = workspace.workspaceFolders[i]; - if (isFolderEqual(folder.uri, folderUriToFind)) { + if (isFolderEqual(folder.uri, folderUriToFind, extHostFileSystemInfo)) { return folder; } } @@ -254,7 +254,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac const validatedDistinctWorkspaceFoldersToAdd: { uri: vscode.Uri, name?: string }[] = []; if (Array.isArray(workspaceFoldersToAdd)) { workspaceFoldersToAdd.forEach(folderToAdd => { - if (URI.isUri(folderToAdd.uri) && !validatedDistinctWorkspaceFoldersToAdd.some(f => isFolderEqual(f.uri, folderToAdd.uri))) { + if (URI.isUri(folderToAdd.uri) && !validatedDistinctWorkspaceFoldersToAdd.some(f => isFolderEqual(f.uri, folderToAdd.uri, this._extHostFileSystemInfo))) { validatedDistinctWorkspaceFoldersToAdd.push({ uri: folderToAdd.uri, name: folderToAdd.name || basenameOrAuthority(folderToAdd.uri) }); } }); @@ -283,13 +283,13 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac for (let i = 0; i < newWorkspaceFolders.length; i++) { const folder = newWorkspaceFolders[i]; - if (newWorkspaceFolders.some((otherFolder, index) => index !== i && isFolderEqual(folder.uri, otherFolder.uri))) { + if (newWorkspaceFolders.some((otherFolder, index) => index !== i && isFolderEqual(folder.uri, otherFolder.uri, this._extHostFileSystemInfo))) { return false; // cannot add the same folder multiple times } } newWorkspaceFolders.forEach((f, index) => f.index = index); // fix index - const { added, removed } = delta(currentWorkspaceFolders, newWorkspaceFolders, compareWorkspaceFolderByUriAndNameAndIndex); + const { added, removed } = delta(currentWorkspaceFolders, newWorkspaceFolders, compareWorkspaceFolderByUriAndNameAndIndex, this._extHostFileSystemInfo); if (added.length === 0 && removed.length === 0) { return false; // nothing actually changed } diff --git a/src/vs/workbench/api/common/shared/workspaceContains.ts b/src/vs/workbench/api/common/shared/workspaceContains.ts index 629c9994a..74a283dce 100644 --- a/src/vs/workbench/api/common/shared/workspaceContains.ts +++ b/src/vs/workbench/api/common/shared/workspaceContains.ts @@ -119,8 +119,7 @@ export function checkGlobFileExists( const queryBuilder = instantiationService.createInstance(QueryBuilder); const query = queryBuilder.file(folders.map(folder => toWorkspaceFolder(URI.revive(folder))), { _reason: 'checkExists', - includePattern: includes.join(', '), - expandPatterns: true, + includePattern: includes, exists: true }); diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index bb5ec1cd7..b3424a779 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -39,7 +39,15 @@ export interface RunCommandPipeArgs { args: any[]; } -export type PipeCommand = OpenCommandPipeArgs | StatusPipeArgs | RunCommandPipeArgs | OpenExternalCommandPipeArgs; +export interface ExtensionManagementPipeArgs { + type: 'extensionManagement'; + list?: { showVersions?: boolean, category?: string; }; + install?: string[]; + uninstall?: string[]; + force?: boolean; +} + +export type PipeCommand = OpenCommandPipeArgs | StatusPipeArgs | RunCommandPipeArgs | OpenExternalCommandPipeArgs | ExtensionManagementPipeArgs; export interface ICommandsExecuter { executeCommand(id: string, ...args: any[]): Promise; @@ -95,6 +103,10 @@ export class CLIServerBase { this.runCommand(data, res) .catch(this.logService.error); break; + case 'extensionManagement': + this.manageExtensions(data, res) + .catch(this.logService.error); + break; default: res.writeHead(404); res.write(`Unknown message type: ${data.type}`, err => { @@ -143,14 +155,34 @@ export class CLIServerBase { res.end(); } - private openExternal(data: OpenExternalCommandPipeArgs, res: http.ServerResponse) { + private async openExternal(data: OpenExternalCommandPipeArgs, res: http.ServerResponse) { for (const uri of data.uris) { - this._commands.executeCommand('_workbench.openExternal', URI.parse(uri), { allowTunneling: true }); + await this._commands.executeCommand('_remoteCLI.openExternal', URI.parse(uri), { allowTunneling: true }); } res.writeHead(200); res.end(); } + private async manageExtensions(data: ExtensionManagementPipeArgs, res: http.ServerResponse) { + console.log('server: manageExtensions'); + try { + const toExtOrVSIX = (inputs: string[] | undefined) => inputs?.map(input => /\.vsix$/i.test(input) ? URI.parse(input) : input); + const commandArgs = { + list: data.list, + install: toExtOrVSIX(data.install), + uninstall: toExtOrVSIX(data.uninstall), + force: data.force + }; + const output = await this._commands.executeCommand('_remoteCLI.manageExtensions', commandArgs, { allowTunneling: true }); + res.writeHead(200); + res.write(output); + } catch (e) { + res.writeHead(500); + res.write(String(e)); + } + res.end(); + } + private async getStatus(data: StatusPipeArgs, res: http.ServerResponse) { try { const status = await this._commands.executeCommand('_issues.getSystemStatus'); diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index ff76f109b..992c63777 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -104,7 +104,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { cwdForPrepareCommand = args.cwd; } - terminal.show(); + terminal.show(true); const shellProcessId = await terminal.processId; diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 3d92a6991..d9377b68c 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as performance from 'vs/base/common/performance'; import { createApiFactoryAndRegisterActors } from 'vs/workbench/api/common/extHost.api.impl'; import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; import { MainContext } from 'vs/workbench/api/common/extHost.protocol'; @@ -13,7 +14,7 @@ import { ExtHostDownloadService } from 'vs/workbench/api/node/extHostDownloadSer import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; class NodeModuleRequireInterceptor extends RequireInterceptor { @@ -62,10 +63,12 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // Module loading tricks const interceptor = this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, this._registry); await interceptor.install(); + performance.mark('code/extHost/didInitAPI'); // Do this when extension service exists, but extensions are not being activated yet. const configProvider = await this._extHostConfiguration.getConfigProvider(); await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._logService, this._mainThreadTelemetryProxy, this._initData); + performance.mark('code/extHost/didInitProxyResolver'); // Use IPC messages to forward console-calls, note that the console is // already patched to use`process.send()` @@ -84,7 +87,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { return extensionDescription.main; } - protected _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + protected _loadCommonJSModule(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { if (module.scheme !== Schemas.file) { throw new Error(`Cannot load URI: '${module}', must be of file-scheme`); } @@ -93,10 +96,16 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { this._logService.info(`ExtensionService#loadCommonJSModule ${module.toString(true)}`); this._logService.flush(); try { + if (extensionId) { + performance.mark(`code/extHost/willLoadExtensionCode/${extensionId.value}`); + } r = require.__$__nodeRequire(module.fsPath); } catch (e) { return Promise.reject(e); } finally { + if (extensionId) { + performance.mark(`code/extHost/didLoadExtensionCode/${extensionId.value}`); + } activationTimesBuilder.codeLoadingStop(); } return Promise.resolve(r); diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 9d5951b84..260ace16b 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -50,11 +50,12 @@ export class ExtHostTask extends ExtHostTaskBase { } public async executeTask(extension: IExtensionDescription, task: vscode.Task): Promise { - if (!task.execution) { + const tTask = (task as types.Task); + + if (!task.execution && (tTask._id === undefined)) { throw new Error('Tasks to execute must include an execution'); } - const tTask = (task as types.Task); // We have a preserved ID. So the task didn't change. if (tTask._id !== undefined) { // Always get the task execution first to prevent timing issues when retrieving it later @@ -121,7 +122,7 @@ export class ExtHostTask extends ExtHostTaskBase { private async getVariableResolver(workspaceFolders: vscode.WorkspaceFolder[]): Promise { if (this._variableResolver === undefined) { const configProvider = await this._configurationService.getConfigProvider(); - this._variableResolver = new ExtHostVariableResolverService(workspaceFolders, this._editorService, configProvider, process.env as IProcessEnvironment); + this._variableResolver = new ExtHostVariableResolverService(workspaceFolders, this._editorService, configProvider, process.env as IProcessEnvironment, this.workspaceService); } return this._variableResolver; } diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 01a34ff3f..adad271b3 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -17,13 +17,15 @@ import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/ext import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; import { ExtHostDocumentsAndEditors, IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { getSystemShell, detectAvailableShells } from 'vs/workbench/contrib/terminal/node/terminal'; +import { detectAvailableShells } from 'vs/workbench/contrib/terminal/node/terminal'; import { getMainProcessParentEnv } from 'vs/workbench/contrib/terminal/node/terminalEnvironment'; import { BaseExtHostTerminalService, ExtHostTerminal } from 'vs/workbench/api/common/extHostTerminalService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { getSystemShell, getSystemShellSync } from 'vs/base/node/shell'; +import { generateUuid } from 'vs/base/common/uuid'; export class ExtHostTerminalService extends BaseExtHostTerminalService { @@ -32,6 +34,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { // TODO: Pull this from main side private _isWorkspaceShellAllowed: boolean = false; + private _defaultShell: string | undefined; constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @@ -42,20 +45,26 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { @IExtHostInitDataService private _extHostInitDataService: IExtHostInitDataService ) { super(true, extHostRpc); + + // Getting the SystemShell is an async operation, however, the ExtHost terminal service is mostly synchronous + // and the API `vscode.env.shell` is also synchronous. The default shell _should_ be set when extensions are + // starting up but if not, we run getSystemShellSync below which gets a sane default. + getSystemShell(platform.platform).then(s => this._defaultShell = s); + this._updateLastActiveWorkspace(); this._updateVariableResolver(); this._registerListeners(); } public createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { - const terminal = new ExtHostTerminal(this._proxy, { name, shellPath, shellArgs }, name); + const terminal = new ExtHostTerminal(this._proxy, generateUuid(), { name, shellPath, shellArgs }, name); this._terminals.push(terminal); terminal.create(shellPath, shellArgs); return terminal; } public createTerminalFromOptions(options: vscode.TerminalOptions, isFeatureTerminal?: boolean): vscode.Terminal { - const terminal = new ExtHostTerminal(this._proxy, options, options.name); + const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); this._terminals.push(terminal); terminal.create( withNullAsUndefined(options.shellPath), @@ -76,10 +85,11 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { .inspect(key.substr(key.lastIndexOf('.') + 1)); return this._apiInspectConfigToPlain(setting); }; + return terminalEnvironment.getDefaultShell( fetchSetting, this._isWorkspaceShellAllowed, - getSystemShell(platform.platform), + this._defaultShell ?? getSystemShellSync(platform.platform), process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), process.env.windir, terminalEnvironment.createVariableResolver(this._lastActiveWorkspace, this._variableResolver), @@ -139,7 +149,8 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { executable: shellLaunchConfigDto.executable, args: shellLaunchConfigDto.args, cwd: typeof shellLaunchConfigDto.cwd === 'string' ? shellLaunchConfigDto.cwd : URI.revive(shellLaunchConfigDto.cwd), - env: shellLaunchConfigDto.env + env: shellLaunchConfigDto.env, + flowControl: shellLaunchConfigDto.flowControl }; // Merge in shell and args from settings diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 922451d10..b7a058768 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -12,12 +12,16 @@ import { URI } from 'vs/base/common/uri'; import { exec } from 'child_process'; import * as resources from 'vs/base/common/resources'; import * as fs from 'fs'; +import * as pfs from 'vs/base/node/pfs'; import { isLinux } from 'vs/base/common/platform'; import { IExtHostTunnelService, TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; -import { asPromise } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { TunnelOptions, TunnelCreationOptions } from 'vs/platform/remote/common/tunnel'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { promisify } from 'util'; +import { MovingAverage } from 'vs/base/common/numbers'; +import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { ILogService } from 'vs/platform/log/common/log'; class ExtensionTunnel implements vscode.Tunnel { private _onDispose: Emitter = new Emitter(); @@ -26,14 +30,106 @@ class ExtensionTunnel implements vscode.Tunnel { constructor( public readonly remoteAddress: { port: number, host: string }, public readonly localAddress: { port: number, host: string } | string, - private readonly _dispose: () => void) { } + private readonly _dispose: () => Promise) { } - dispose(): void { + dispose(): Promise { this._onDispose.fire(); - this._dispose(); + return this._dispose(); } } +export function getSockets(stdout: string): { pid: number, socket: number }[] { + const lines = stdout.trim().split('\n'); + const mapped: { pid: number, socket: number }[] = []; + lines.forEach(line => { + const match = /\/proc\/(\d+)\/fd\/\d+ -> socket:\[(\d+)\]/.exec(line)!; + if (match && match.length >= 3) { + mapped.push({ + pid: parseInt(match[1], 10), + socket: parseInt(match[2], 10) + }); + } + }); + return mapped; +} + +export function loadListeningPorts(...stdouts: string[]): { socket: number, ip: string, port: number }[] { + const table = ([] as Record[]).concat(...stdouts.map(loadConnectionTable)); + return [ + ...new Map( + table.filter(row => row.st === '0A') + .map(row => { + const address = row.local_address.split(':'); + return { + socket: parseInt(row.inode, 10), + ip: parseIpAddress(address[0]), + port: parseInt(address[1], 16) + }; + }).map(port => [port.ip + ':' + port.port, port]) + ).values() + ]; +} + +export function parseIpAddress(hex: string): string { + let result = ''; + if (hex.length === 8) { + for (let i = hex.length - 2; i >= 0; i -= 2) { + result += parseInt(hex.substr(i, 2), 16); + if (i !== 0) { + result += '.'; + } + } + } else { + for (let i = hex.length - 4; i >= 0; i -= 4) { + result += parseInt(hex.substr(i, 4), 16).toString(16); + if (i !== 0) { + result += ':'; + } + } + } + return result; +} + +export function loadConnectionTable(stdout: string): Record[] { + const lines = stdout.trim().split('\n'); + const names = lines.shift()!.trim().split(/\s+/) + .filter(name => name !== 'rx_queue' && name !== 'tm->when'); + const table = lines.map(line => line.trim().split(/\s+/).reduce((obj, value, i) => { + obj[names[i] || i] = value; + return obj; + }, {} as Record)); + return table; +} + +function knownExcludeCmdline(command: string): boolean { + return !!command.match(/.*\.vscode-server-[a-zA-Z]+\/bin.*/) + || (command.indexOf('out/vs/server/main.js') !== -1) + || (command.indexOf('_productName=VSCode') !== -1); +} + +export async function findPorts(tcp: string, tcp6: string, procSockets: string, processes: { pid: number, cwd: string, cmd: string }[]): Promise { + const connections: { socket: number, ip: string, port: number }[] = loadListeningPorts(tcp, tcp6); + const sockets = getSockets(procSockets); + + const socketMap = sockets.reduce((m, socket) => { + m[socket.socket] = socket; + return m; + }, {} as Record); + const processMap = processes.reduce((m, process) => { + m[process.pid] = process; + return m; + }, {} as Record); + + const ports: CandidatePort[] = []; + connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => { + const command = processMap[socketMap[socket].pid].cmd; + if (!knownExcludeCmdline(command)) { + ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd, pid: socketMap[socket].pid }); + } + }); + return ports; +} + export class ExtHostTunnelService extends Disposable implements IExtHostTunnelService { readonly _serviceBrand: undefined; private readonly _proxy: MainThreadTunnelServiceShape; @@ -42,15 +138,17 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe private _extensionTunnels: Map> = new Map(); private _onDidChangeTunnels: Emitter = new Emitter(); onDidChangeTunnels: vscode.Event = this._onDidChangeTunnels.event; + private _candidateFindingEnabled: boolean = false; constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, - @IExtHostInitDataService initData: IExtHostInitDataService + @IExtHostInitDataService initData: IExtHostInitDataService, + @ILogService private readonly logService: ILogService ) { super(); this._proxy = extHostRpc.getProxy(MainContext.MainThreadTunnelService); - if (initData.remote.isRemote && initData.remote.authority) { - this.registerCandidateFinder(); + if (isLinux && initData.remote.isRemote && initData.remote.authority) { + this._proxy.$setCandidateFinder(); } } @@ -70,18 +168,30 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe return this._proxy.$getTunnels(); } - registerCandidateFinder(): void { - // Every two seconds, scan to see if the candidate ports have changed; - if (isLinux) { - let oldPorts: { host: string, port: number, detail: string }[] | undefined = undefined; - setInterval(async () => { - const newPorts = await this.findCandidatePorts(); - if (!oldPorts || (JSON.stringify(oldPorts) !== JSON.stringify(newPorts))) { - oldPorts = newPorts; - this._proxy.$onFoundNewCandidates(oldPorts.filter(async (candidate) => await this._showCandidatePort(candidate.host, candidate.port, candidate.detail))); - return; - } - }, 2000); + private calculateDelay(movingAverage: number) { + // Some local testing indicated that the moving average might be between 50-100 ms. + return Math.max(movingAverage * 20, 2000); + } + + async $registerCandidateFinder(enable: boolean): Promise { + if (enable && this._candidateFindingEnabled) { + // already enabled + return; + } + this._candidateFindingEnabled = enable; + // Regularly scan to see if the candidate ports have changed. + let movingAverage = new MovingAverage(); + let oldPorts: { host: string, port: number, detail: string }[] | undefined = undefined; + while (this._candidateFindingEnabled) { + const startTime = new Date().getTime(); + const newPorts = await this.findCandidatePorts(); + const timeTaken = new Date().getTime() - startTime; + movingAverage.update(timeTaken); + if (!oldPorts || (JSON.stringify(oldPorts) !== JSON.stringify(newPorts))) { + oldPorts = newPorts; + await this._proxy.$onFoundNewCandidates(oldPorts); + } + await (new Promise(resolve => setTimeout(() => resolve(), this.calculateDelay(movingAverage.value)))); } } @@ -89,15 +199,18 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe if (provider) { if (provider.showCandidatePort) { this._showCandidatePort = provider.showCandidatePort; + await this._proxy.$setCandidateFilter(); } if (provider.tunnelFactory) { this._forwardPortProvider = provider.tunnelFactory; - await this._proxy.$setTunnelProvider(); + await this._proxy.$setTunnelProvider(provider.tunnelFeatures ?? { + elevation: false, + public: false + }); } } else { this._forwardPortProvider = undefined; } - await this._proxy.$tunnelServiceReady(); return toDisposable(() => { this._forwardPortProvider = undefined; }); @@ -110,7 +223,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe if (silent) { hostMap.get(remote.port)!.disposeListener.dispose(); } - hostMap.get(remote.port)!.tunnel.dispose(); + await hostMap.get(remote.port)!.tunnel.dispose(); hostMap.delete(remote.port); } } @@ -120,31 +233,42 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe this._onDidChangeTunnels.fire(); } - $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined { + async $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise { if (this._forwardPortProvider) { - const providedPort = this._forwardPortProvider(tunnelOptions, tunnelCreationOptions); - if (providedPort !== undefined) { - return asPromise(() => providedPort).then(tunnel => { + try { + this.logService.trace('$forwardPort: Getting tunnel from provider.'); + const providedPort = this._forwardPortProvider(tunnelOptions, tunnelCreationOptions); + this.logService.trace('$forwardPort: Got tunnel promise from provider.'); + if (providedPort !== undefined) { + const tunnel = await providedPort; + this.logService.trace('$forwardPort: Successfully awaited tunnel from provider.'); if (!this._extensionTunnels.has(tunnelOptions.remoteAddress.host)) { this._extensionTunnels.set(tunnelOptions.remoteAddress.host, new Map()); } const disposeListener = this._register(tunnel.onDidDispose(() => this._proxy.$closeTunnel(tunnel.remoteAddress))); this._extensionTunnels.get(tunnelOptions.remoteAddress.host)!.set(tunnelOptions.remoteAddress.port, { tunnel, disposeListener }); - return Promise.resolve(TunnelDto.fromApiTunnel(tunnel)); - }); + return TunnelDto.fromApiTunnel(tunnel); + } else { + this.logService.trace('$forwardPort: Tunnel is undefined'); + } + } catch (e) { + this.logService.trace('$forwardPort: tunnel provider error'); } } return undefined; } + async $applyCandidateFilter(candidates: CandidatePort[]): Promise { + const filter = await Promise.all(candidates.map(candidate => this._showCandidatePort(candidate.host, candidate.port, candidate.detail))); + return candidates.filter((candidate, index) => filter[index]); + } - async findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]> { - const ports: { host: string, port: number, detail: string }[] = []; + async findCandidatePorts(): Promise { let tcp: string = ''; let tcp6: string = ''; try { - tcp = fs.readFileSync('/proc/net/tcp', 'utf8'); - tcp6 = fs.readFileSync('/proc/net/tcp6', 'utf8'); + tcp = await pfs.readFile('/proc/net/tcp', 'utf8'); + tcp6 = await pfs.readFile('/proc/net/tcp6', 'utf8'); } catch (e) { // File reading error. No additional handling needed. } @@ -154,105 +278,24 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe }); })); - const procChildren = fs.readdirSync('/proc'); - const processes: { pid: number, cwd: string, cmd: string }[] = []; + const procChildren = await pfs.readdir('/proc'); + const processes: { + pid: number, cwd: string, cmd: string + }[] = []; for (let childName of procChildren) { try { const pid: number = Number(childName); const childUri = resources.joinPath(URI.file('/proc'), childName); - const childStat = fs.statSync(childUri.fsPath); + const childStat = await pfs.stat(childUri.fsPath); if (childStat.isDirectory() && !isNaN(pid)) { - const cwd = fs.readlinkSync(resources.joinPath(childUri, 'cwd').fsPath); - const cmd = fs.readFileSync(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); + const cwd = await promisify(fs.readlink)(resources.joinPath(childUri, 'cwd').fsPath); + const cmd = await pfs.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); processes.push({ pid, cwd, cmd }); } } catch (e) { // } } - - const connections: { socket: number, ip: string, port: number }[] = this.loadListeningPorts(tcp, tcp6); - const sockets = this.getSockets(procSockets); - - const socketMap = sockets.reduce((m, socket) => { - m[socket.socket] = socket; - return m; - }, {} as Record); - const processMap = processes.reduce((m, process) => { - m[process.pid] = process; - return m; - }, {} as Record); - - connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => { - const command = processMap[socketMap[socket].pid].cmd; - if (!command.match(/.*\.vscode-server-[a-zA-Z]+\/bin.*/) && (command.indexOf('out/vs/server/main.js') === -1)) { - ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd }); - } - }); - - return ports; - } - - private getSockets(stdout: string): { pid: number, socket: number }[] { - const lines = stdout.trim().split('\n'); - const mapped: { pid: number, socket: number }[] = []; - lines.forEach(line => { - const match = /\/proc\/(\d+)\/fd\/\d+ -> socket:\[(\d+)\]/.exec(line)!; - if (match && match.length >= 3) { - mapped.push({ - pid: parseInt(match[1], 10), - socket: parseInt(match[2], 10) - }); - } - }); - return mapped; - } - - private loadListeningPorts(...stdouts: string[]): { socket: number, ip: string, port: number }[] { - const table = ([] as Record[]).concat(...stdouts.map(this.loadConnectionTable)); - return [ - ...new Map( - table.filter(row => row.st === '0A') - .map(row => { - const address = row.local_address.split(':'); - return { - socket: parseInt(row.inode, 10), - ip: this.parseIpAddress(address[0]), - port: parseInt(address[1], 16) - }; - }).map(port => [port.ip + ':' + port.port, port]) - ).values() - ]; - } - - private parseIpAddress(hex: string): string { - let result = ''; - if (hex.length === 8) { - for (let i = hex.length - 2; i >= 0; i -= 2) { - result += parseInt(hex.substr(i, 2), 16); - if (i !== 0) { - result += '.'; - } - } - } else { - for (let i = hex.length - 4; i >= 0; i -= 4) { - result += parseInt(hex.substr(i, 4), 16).toString(16); - if (i !== 0) { - result += ':'; - } - } - } - return result; - } - - private loadConnectionTable(stdout: string): Record[] { - const lines = stdout.trim().split('\n'); - const names = lines.shift()!.trim().split(/\s+/) - .filter(name => name !== 'rx_queue' && name !== 'tm->when'); - const table = lines.map(line => line.trim().split(/\s+/).reduce((obj, value, i) => { - obj[names[i] || i] = value; - return obj; - }, {} as Record)); - return table; + return findPorts(tcp, tcp6, procSockets, processes); } } diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index 021af6e0f..6ebbb92e4 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -8,10 +8,38 @@ import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHost import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { URI } from 'vs/base/common/uri'; import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; import { timeout } from 'vs/base/common/async'; +namespace TrustedFunction { + + // workaround a chrome issue not allowing to create new functions + // see https://github.com/w3c/webappsec-trusted-types/wiki/Trusted-Types-for-function-constructor + const ttpTrustedFunction = self.trustedTypes?.createPolicy('TrustedFunctionWorkaround', { + createScript: (_, ...args: string[]) => { + args.forEach((arg) => { + if (!self.trustedTypes?.isScript(arg)) { + throw new Error('TrustedScripts only, please'); + } + }); + // NOTE: This is insecure without parsing the arguments and body, + // Malicious inputs can escape the function body and execute immediately! + const fnArgs = args.slice(0, -1).join(','); + const fnBody = args.pop()!.toString(); + const body = `(function anonymous(${fnArgs}) {\n${fnBody}\n})`; + return body; + } + }); + + export function create(...args: string[]): Function { + if (!ttpTrustedFunction) { + return new Function(...args); + } + return self.eval(ttpTrustedFunction.createScript('', ...args) as unknown as string); + } +} + class WorkerRequireInterceptor extends RequireInterceptor { _installInterceptor() { } @@ -35,6 +63,8 @@ class WorkerRequireInterceptor extends RequireInterceptor { export class ExtHostExtensionService extends AbstractExtHostExtensionService { readonly extensionRuntime = ExtensionRuntime.Webworker; + private static _ttpExtensionScripts = self.trustedTypes?.createPolicy('ExtensionScripts', { createScript: source => source }); + private _fakeModules?: WorkerRequireInterceptor; protected async _beforeAlmostReadyToRunExtensions(): Promise { @@ -42,6 +72,8 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { const apiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors); this._fakeModules = this._instaService.createInstance(WorkerRequireInterceptor, apiFactory, this._registry); await this._fakeModules.install(); + performance.mark('code/extHost/didInitAPI'); + await this._waitForDebuggerAttachment(); } @@ -49,10 +81,16 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { return extensionDescription.browser; } - protected async _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + protected async _loadCommonJSModule(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { module = module.with({ path: ensureSuffix(module.path, '.js') }); + if (extensionId) { + performance.mark(`code/extHost/willFetchExtensionCode/${extensionId.value}`); + } const response = await fetch(module.toString(true)); + if (extensionId) { + performance.mark(`code/extHost/didFetchExtensionCode/${extensionId.value}`); + } if (response.status !== 200) { throw new Error(response.statusText); @@ -63,7 +101,25 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // Here we append #vscode-extension to serve as a marker, such that source maps // can be adjusted for the extra wrapping function. const sourceURL = `${module.toString(true)}#vscode-extension`; - const initFn = new Function('module', 'exports', 'require', `${source}\n//# sourceURL=${sourceURL}`); + const fullSource = `${source}\n//# sourceURL=${sourceURL}`; + let initFn: Function; + try { + initFn = TrustedFunction.create( + ExtHostExtensionService._ttpExtensionScripts?.createScript('module') as unknown as string ?? 'module', + ExtHostExtensionService._ttpExtensionScripts?.createScript('exports') as unknown as string ?? 'exports', + ExtHostExtensionService._ttpExtensionScripts?.createScript('require') as unknown as string ?? 'require', + ExtHostExtensionService._ttpExtensionScripts?.createScript(fullSource) as unknown as string ?? fullSource + ); + } catch (err) { + if (extensionId) { + console.error(`Loading code for extension ${extensionId.value} failed: ${err.message}`); + } else { + console.error(`Loading code failed: ${err.message}`); + } + console.error(`${module.toString(true)}${typeof err.line === 'number' ? ` line ${err.line}` : ''}${typeof err.column === 'number' ? ` column ${err.column}` : ''}`); + console.error(err); + throw err; + } // define commonjs globals: `module`, `exports`, and `require` const _exports = {}; @@ -78,9 +134,15 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { try { activationTimesBuilder.codeLoadingStart(); + if (extensionId) { + performance.mark(`code/extHost/willLoadExtensionCode/${extensionId.value}`); + } initFn(_module, _exports, _require); return (_module.exports !== _exports ? _module.exports : _exports); } finally { + if (extensionId) { + performance.mark(`code/extHost/didLoadExtensionCode/${extensionId.value}`); + } activationTimesBuilder.codeLoadingStop(); } } diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index cdd2cdfc3..3515af949 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -8,25 +8,21 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Action } from 'vs/base/common/actions'; import { SyncActionDescriptor, MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as WorkbenchExtensions, CATEGORIES } from 'vs/workbench/common/actions'; -import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; -import { IEditorGroupsService, GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { getMenuBarVisibility } from 'vs/platform/windows/common/windows'; import { isWindows, isLinux, isWeb } from 'vs/base/common/platform'; import { IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { InEditorZenModeContext, IsCenteredLayoutContext, EditorAreaVisibleContext } from 'vs/workbench/common/editor'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; -import { IViewDescriptorService, IViewsService, FocusedViewContext, ViewContainerLocation, IViewDescriptor } from 'vs/workbench/common/views'; +import { IViewDescriptorService, IViewsService, FocusedViewContext, ViewContainerLocation, IViewDescriptor, ViewContainerLocationToString } from 'vs/workbench/common/views'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { Codicon } from 'vs/base/common/codicons'; const registry = Registry.as(WorkbenchExtensions.WorkbenchActions); @@ -125,54 +121,6 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 3 }); -// --- Toggle Editor Layout - -export class ToggleEditorLayoutAction extends Action { - - static readonly ID = 'workbench.action.toggleEditorGroupLayout'; - static readonly LABEL = nls.localize('flipLayout', "Toggle Vertical/Horizontal Editor Layout"); - - private readonly toDispose = this._register(new DisposableStore()); - - constructor( - id: string, - label: string, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService - ) { - super(id, label); - - this.class = Codicon.editorLayout.classNames; - this.updateEnablement(); - - this.registerListeners(); - } - - private registerListeners(): void { - this.toDispose.add(this.editorGroupService.onDidAddGroup(() => this.updateEnablement())); - this.toDispose.add(this.editorGroupService.onDidRemoveGroup(() => this.updateEnablement())); - } - - private updateEnablement(): void { - this.enabled = this.editorGroupService.count > 1; - } - - async run(): Promise { - const newOrientation = (this.editorGroupService.orientation === GroupOrientation.VERTICAL) ? GroupOrientation.HORIZONTAL : GroupOrientation.VERTICAL; - this.editorGroupService.setGroupOrientation(newOrientation); - } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleEditorLayoutAction, { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_0, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_0 } }), 'View: Toggle Vertical/Horizontal Editor Layout', CATEGORIES.View.value); - -MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: 'z_flip', - command: { - id: ToggleEditorLayoutAction.ID, - title: nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Flip &&Layout") - }, - order: 1 -}); - // --- Toggle Sidebar Position export class ToggleSidebarPositionAction extends Action { @@ -203,7 +151,64 @@ export class ToggleSidebarPositionAction extends Action { } } -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleSidebarPositionAction), 'View: Toggle Side Bar Position', CATEGORIES.View.value); +registerAction2(class extends Action2 { + constructor() { + super({ + id: ToggleSidebarPositionAction.ID, + title: { value: nls.localize('toggleSidebarPosition', "Toggle Side Bar Position"), original: 'Toggle Side Bar Position' }, + category: CATEGORIES.View, + f1: true + }); + } + run(accessor: ServicesAccessor) { + accessor.get(IInstantiationService).createInstance(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.LABEL).run(); + } +}); +MenuRegistry.appendMenuItems([{ + id: MenuId.ViewContainerTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: ToggleSidebarPositionAction.ID, + title: nls.localize('move sidebar right', "Move Side Bar Right") + }, + when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 1 + } +}, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: ToggleSidebarPositionAction.ID, + title: nls.localize('move sidebar right', "Move Side Bar Right") + }, + when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 1 + } +}, { + id: MenuId.ViewContainerTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: ToggleSidebarPositionAction.ID, + title: nls.localize('move sidebar left', "Move Side Bar Left") + }, + when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 1 + } +}, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: ToggleSidebarPositionAction.ID, + title: nls.localize('move sidebar left', "Move Side Bar Left") + }, + when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 1 + } +}]); MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { group: '3_workbench_layout_move', @@ -256,27 +261,6 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 5 }); -export class ToggleSidebarVisibilityAction extends Action { - - static readonly ID = 'workbench.action.toggleSidebarVisibility'; - static readonly LABEL = nls.localize('toggleSidebar', "Toggle Side Bar Visibility"); - - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService - ) { - super(id, label); - } - - async run(): Promise { - const hideSidebar = this.layoutService.isVisible(Parts.SIDEBAR_PART); - this.layoutService.setSideBarHidden(hideSidebar); - } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleSidebarVisibilityAction, { primary: KeyMod.CtrlCmd | KeyCode.KEY_B }), 'View: Toggle Side Bar Visibility', CATEGORIES.View.value); - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { group: '2_appearance', title: nls.localize({ key: 'miAppearance', comment: ['&& denotes a mnemonic'] }, "&&Appearance"), @@ -284,10 +268,53 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { order: 1 }); +export const TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID = 'workbench.action.toggleSidebarVisibility'; +registerAction2(class extends Action2 { + constructor() { + super({ + id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + title: { value: nls.localize('toggleSidebar', "Toggle Side Bar Visibility"), original: 'Toggle Side Bar Visibility' }, + category: CATEGORIES.View, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KEY_B + } + }); + } + run(accessor: ServicesAccessor) { + const layoutService = accessor.get(IWorkbenchLayoutService); + layoutService.setSideBarHidden(layoutService.isVisible(Parts.SIDEBAR_PART)); + } +}); +MenuRegistry.appendMenuItems([{ + id: MenuId.ViewContainerTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + title: nls.localize('compositePart.hideSideBarLabel', "Hide Side Bar"), + }, + when: ContextKeyExpr.and(SideBarVisibleContext, ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 2 + } +}, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + title: nls.localize('compositePart.hideSideBarLabel', "Hide Side Bar"), + }, + when: ContextKeyExpr.and(SideBarVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 2 + } +}]); + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { group: '2_workbench_layout', command: { - id: ToggleSidebarVisibilityAction.ID, + id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, title: nls.localize({ key: 'miShowSidebar', comment: ['&& denotes a mnemonic'] }, "Show &&Side Bar"), toggled: SideBarVisibleContext }, @@ -413,32 +440,16 @@ export class ToggleMenuBarAction extends Action { static readonly ID = 'workbench.action.toggleMenuBar'; static readonly LABEL = nls.localize('toggleMenuBar', "Toggle Menu Bar"); - private static readonly menuBarVisibilityKey = 'window.menuBarVisibility'; - constructor( id: string, label: string, - @IConfigurationService private readonly configurationService: IConfigurationService + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService ) { super(id, label); } - run(): Promise { - let currentVisibilityValue = getMenuBarVisibility(this.configurationService); - if (typeof currentVisibilityValue !== 'string') { - currentVisibilityValue = 'default'; - } - - let newVisibilityValue: string; - if (currentVisibilityValue === 'visible' || currentVisibilityValue === 'default') { - newVisibilityValue = 'toggle'; - } else if (currentVisibilityValue === 'compact') { - newVisibilityValue = 'hidden'; - } else { - newVisibilityValue = (isWeb && currentVisibilityValue === 'hidden') ? 'compact' : 'default'; - } - - return this.configurationService.updateValue(ToggleMenuBarAction.menuBarVisibilityKey, newVisibilityValue, ConfigurationTarget.USER); + async run(): Promise { + this.layoutService.toggleMenuBar(); } } @@ -479,19 +490,18 @@ export class ResetViewLocationsAction extends Action { registry.registerWorkbenchAction(SyncActionDescriptor.from(ResetViewLocationsAction), 'View: Reset View Locations', CATEGORIES.View.value); // --- Toggle View with Command -export abstract class ToggleViewAction extends Action { +export class ToggleViewAction extends Action { constructor( id: string, label: string, private readonly viewId: string, - protected viewsService: IViewsService, - protected viewDescriptorService: IViewDescriptorService, - protected contextKeyService: IContextKeyService, - private layoutService: IWorkbenchLayoutService, - cssClass?: string + @IViewsService protected viewsService: IViewsService, + @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService, + @IContextKeyService protected contextKeyService: IContextKeyService, + @IWorkbenchLayoutService private layoutService: IWorkbenchLayoutService, ) { - super(id, label, cssClass); + super(id, label); } async run(): Promise { diff --git a/src/vs/workbench/browser/actions/navigationActions.ts b/src/vs/workbench/browser/actions/navigationActions.ts index 5833c3291..20accba76 100644 --- a/src/vs/workbench/browser/actions/navigationActions.ts +++ b/src/vs/workbench/browser/actions/navigationActions.ts @@ -17,7 +17,6 @@ import { IWorkbenchActionRegistry, Extensions, CATEGORIES } from 'vs/workbench/c import { Direction } from 'vs/base/browser/ui/grid/grid'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { isAncestor } from 'vs/base/browser/dom'; abstract class BaseNavigationAction extends Action { @@ -215,7 +214,8 @@ function findVisibleNeighbour(layoutService: IWorkbenchLayoutService, part: Part } function focusNextOrPreviousPart(layoutService: IWorkbenchLayoutService, editorService: IEditorService, next: boolean): void { - const currentlyFocusedPart = isActiveElementInNotebookEditor(editorService) ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.EDITOR_PART) ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.ACTIVITYBAR_PART) ? Parts.ACTIVITYBAR_PART : + const editorFocused = editorService.activeEditorPane?.hasFocus(); + const currentlyFocusedPart = editorFocused ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.ACTIVITYBAR_PART) ? Parts.ACTIVITYBAR_PART : layoutService.hasFocus(Parts.STATUSBAR_PART) ? Parts.STATUSBAR_PART : layoutService.hasFocus(Parts.SIDEBAR_PART) ? Parts.SIDEBAR_PART : layoutService.hasFocus(Parts.PANEL_PART) ? Parts.PANEL_PART : undefined; let partToFocus = Parts.EDITOR_PART; if (currentlyFocusedPart) { @@ -225,17 +225,6 @@ function focusNextOrPreviousPart(layoutService: IWorkbenchLayoutService, editorS layoutService.focusPart(partToFocus); } -function isActiveElementInNotebookEditor(editorService: IEditorService): boolean { - const activeEditorPane = editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean } | undefined; - if (activeEditorPane?.isNotebookEditor) { - const control = editorService.activeEditorPane?.getControl() as { getDomNode(): HTMLElement; getOverflowContainerDomNode(): HTMLElement; }; - const activeElement = document.activeElement; - return isAncestor(activeElement, control.getDomNode()) || isAncestor(activeElement, control.getOverflowContainerDomNode()); - } - - return false; -} - export class FocusNextPart extends Action { static readonly ID = 'workbench.action.focusNextPart'; static readonly LABEL = nls.localize('focusNextPart', "Focus Next Part"); diff --git a/src/vs/workbench/browser/actions/textInputActions.ts b/src/vs/workbench/browser/actions/textInputActions.ts index 8b63f052b..606099d23 100644 --- a/src/vs/workbench/browser/actions/textInputActions.ts +++ b/src/vs/workbench/browser/actions/textInputActions.ts @@ -76,23 +76,26 @@ export class TextInputActionsProvider extends Disposable implements IWorkbenchCo // Context menu support in input/textarea this.layoutService.container.addEventListener('contextmenu', e => this.onContextMenu(e)); - } private onContextMenu(e: MouseEvent): void { - if (e.target instanceof HTMLElement) { - const target = e.target; - if (target.nodeName && (target.nodeName.toLowerCase() === 'input' || target.nodeName.toLowerCase() === 'textarea')) { - EventHelper.stop(e, true); - - this.contextMenuService.showContextMenu({ - getAnchor: () => e, - getActions: () => this.textInputActions, - getActionsContext: () => target, - onHide: () => target.focus() // fixes https://github.com/microsoft/vscode/issues/52948 - }); - } + if (e.defaultPrevented) { + return; // make sure to not show these actions by accident if component indicated to prevent } + + const target = e.target; + if (!(target instanceof HTMLElement) || (target.nodeName.toLowerCase() !== 'input' && target.nodeName.toLowerCase() !== 'textarea')) { + return; // only for inputs or textareas + } + + EventHelper.stop(e, true); + + this.contextMenuService.showContextMenu({ + getAnchor: () => e, + getActions: () => this.textInputActions, + getActionsContext: () => target, + onHide: () => target.focus() // fixes https://github.com/microsoft/vscode/issues/52948 + }); } } diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index e5fdcdbca..926527871 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -33,7 +33,7 @@ import { ResourceMap } from 'vs/base/common/map'; import { Codicon } from 'vs/base/common/codicons'; import { isHTMLElement } from 'vs/base/browser/dom'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export const inRecentFilesPickerContextKey = 'inRecentFilesPicker'; @@ -49,12 +49,17 @@ abstract class BaseOpenRecentAction extends Action { tooltip: nls.localize('remove', "Remove from Recently Opened") }; - private readonly dirtyRecentlyOpened: IQuickInputButton = { + private readonly dirtyRecentlyOpenedFolder: IQuickInputButton = { iconClass: 'dirty-workspace ' + Codicon.closeDirty.classNames, - tooltip: nls.localize('dirtyRecentlyOpened', "Workspace With Dirty Files"), + tooltip: nls.localize('dirtyRecentlyOpenedFolder', "Folder With Unsaved Files"), alwaysVisible: true }; + private readonly dirtyRecentlyOpenedWorkspace: IQuickInputButton = { + ...this.dirtyRecentlyOpenedFolder, + tooltip: nls.localize('dirtyRecentlyOpenedWorkspace', "Workspace With Unsaved Files"), + }; + constructor( id: string, label: string, @@ -77,7 +82,9 @@ abstract class BaseOpenRecentAction extends Action { const recentlyOpened = await this.workspacesService.getRecentlyOpened(); const dirtyWorkspacesAndFolders = await this.workspacesService.getDirtyWorkspaces(); - // Identify all folders and workspaces with dirty files + let hasWorkspaces = false; + + // Identify all folders and workspaces with unsaved files const dirtyFolders = new ResourceMap(); const dirtyWorkspaces = new ResourceMap(); for (const dirtyWorkspace of dirtyWorkspacesAndFolders) { @@ -85,6 +92,7 @@ abstract class BaseOpenRecentAction extends Action { dirtyFolders.set(dirtyWorkspace, true); } else { dirtyWorkspaces.set(dirtyWorkspace.configPath, dirtyWorkspace); + hasWorkspaces = true; } } @@ -96,6 +104,7 @@ abstract class BaseOpenRecentAction extends Action { recentFolders.set(recent.folderUri, true); } else { recentWorkspaces.set(recent.workspace.configPath, recent.workspace); + hasWorkspaces = true; } } @@ -124,7 +133,7 @@ abstract class BaseOpenRecentAction extends Action { let keyMods: IKeyMods | undefined; - const workspaceSeparator: IQuickPickSeparator = { type: 'separator', label: nls.localize('workspaces', "workspaces") }; + const workspaceSeparator: IQuickPickSeparator = { type: 'separator', label: hasWorkspaces ? nls.localize('workspacesAndFolders', "folders & workspaces") : nls.localize('folders', "folders") }; const fileSeparator: IQuickPickSeparator = { type: 'separator', label: nls.localize('files', "files") }; const picks = [workspaceSeparator, ...workspacePicks, fileSeparator, ...filePicks]; @@ -143,13 +152,14 @@ abstract class BaseOpenRecentAction extends Action { context.removeItem(); } - // Dirty Workspace - else if (context.button === this.dirtyRecentlyOpened) { + // Dirty Folder/Workspace + else if (context.button === this.dirtyRecentlyOpenedFolder || context.button === this.dirtyRecentlyOpenedWorkspace) { + const isDirtyWorkspace = context.button === this.dirtyRecentlyOpenedWorkspace; const result = await this.dialogService.confirm({ type: 'question', - title: nls.localize('dirtyWorkspace', "Workspace with Dirty Files"), - message: nls.localize('dirtyWorkspaceConfirm', "Do you want to open the workspace to review the dirty files?"), - detail: nls.localize('dirtyWorkspaceConfirmDetail', "Workspaces with dirty files cannot be removed until all dirty files have been saved or reverted.") + title: isDirtyWorkspace ? nls.localize('dirtyWorkspace', "Workspace with Unsaved Files") : nls.localize('dirtyFolder', "Folder with Unsaved Files"), + message: isDirtyWorkspace ? nls.localize('dirtyWorkspaceConfirm', "Do you want to open the workspace to review the unsaved files?") : nls.localize('dirtyFolderConfirm', "Do you want to open the folder to review the unsaved files?"), + detail: isDirtyWorkspace ? nls.localize('dirtyWorkspaceConfirmDetail', "Workspaces with unsaved files cannot be removed until all unsaved files have been saved or reverted.") : nls.localize('dirtyFolderConfirmDetail', "Folders with unsaved files cannot be removed until all unsaved files have been saved or reverted.") }); if (result.confirmed) { @@ -170,6 +180,7 @@ abstract class BaseOpenRecentAction extends Action { let iconClasses: string[]; let fullLabel: string | undefined; let resource: URI | undefined; + let isWorkspace = false; // Folder if (isRecentFolder(recent)) { @@ -185,6 +196,7 @@ abstract class BaseOpenRecentAction extends Action { iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.ROOT_FOLDER); openable = { workspaceUri: resource }; fullLabel = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: true }); + isWorkspace = true; } // File @@ -200,9 +212,9 @@ abstract class BaseOpenRecentAction extends Action { return { iconClasses, label: name, - ariaLabel: isDirty ? nls.localize('recentDirtyAriaLabel', "{0}, dirty workspace", name) : name, + ariaLabel: isDirty ? isWorkspace ? nls.localize('recentDirtyWorkspaceAriaLabel', "{0}, workspace with unsaved changes", name) : nls.localize('recentDirtyFolderAriaLabel', "{0}, folder with unsaved changes", name) : name, description: parentPath, - buttons: isDirty ? [this.dirtyRecentlyOpened] : [this.removeFromRecentlyOpened], + buttons: isDirty ? [isWorkspace ? this.dirtyRecentlyOpenedWorkspace : this.dirtyRecentlyOpenedFolder] : [this.removeFromRecentlyOpened], openable, resource }; @@ -405,7 +417,7 @@ CommandsRegistry.registerCommand('workbench.action.toggleConfirmBeforeClose', ac const configurationService = accessor.get(IConfigurationService); const setting = configurationService.inspect<'always' | 'keyboardOnly' | 'never'>('window.confirmBeforeClose').userValue; - return configurationService.updateValue('window.confirmBeforeClose', setting === 'never' ? 'keyboardOnly' : 'never', ConfigurationTarget.USER); + return configurationService.updateValue('window.confirmBeforeClose', setting === 'never' ? 'keyboardOnly' : 'never'); }); // --- Menu Registration diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index b73573760..ea67ce4c5 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -114,7 +114,7 @@ export class CloseWorkspaceAction extends Action { async run(): Promise { if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - this.notificationService.info(nls.localize('noWorkspaceOpened', "There is currently no workspace opened in this instance to close.")); + this.notificationService.info(nls.localize('noWorkspaceOrFolderOpened', "There is currently no workspace or folder opened in this instance to close.")); return; } @@ -225,7 +225,7 @@ export class SaveWorkspaceAsAction extends Action { export class DuplicateWorkspaceInNewWindowAction extends Action { static readonly ID = 'workbench.action.duplicateWorkspaceInNewWindow'; - static readonly LABEL = nls.localize('duplicateWorkspaceInNewWindow', "Duplicate Workspace in New Window"); + static readonly LABEL = nls.localize('duplicateWorkspaceInNewWindow', "Duplicate As Workspace in New Window"); constructor( id: string, @@ -259,7 +259,7 @@ registry.registerWorkbenchAction(SyncActionDescriptor.from(AddRootFolderAction), registry.registerWorkbenchAction(SyncActionDescriptor.from(GlobalRemoveRootFolderAction), 'Workspaces: Remove Folder from Workspace...', workspacesCategory); registry.registerWorkbenchAction(SyncActionDescriptor.from(CloseWorkspaceAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) }), 'Workspaces: Close Workspace', workspacesCategory, EmptyWorkspaceSupportContext); registry.registerWorkbenchAction(SyncActionDescriptor.from(SaveWorkspaceAsAction), 'Workspaces: Save Workspace As...', workspacesCategory, EmptyWorkspaceSupportContext); -registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateWorkspaceInNewWindowAction), 'Workspaces: Duplicate Workspace in New Window', workspacesCategory); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateWorkspaceInNewWindowAction), 'Workspaces: Duplicate As Workspace in New Window', workspacesCategory); // --- Menu Registration diff --git a/src/vs/workbench/browser/parts/editor/editorWidgets.ts b/src/vs/workbench/browser/codeeditor.ts similarity index 60% rename from src/vs/workbench/browser/parts/editor/editorWidgets.ts rename to src/vs/workbench/browser/codeeditor.ts index 02601bc97..7bcac2ca0 100644 --- a/src/vs/workbench/browser/parts/editor/editorWidgets.ts +++ b/src/vs/workbench/browser/codeeditor.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Widget } from 'vs/base/browser/ui/widget'; -import { IOverlayWidget, ICodeEditor, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { IOverlayWidget, ICodeEditor, IOverlayWidgetPosition, OverlayWidgetPositionPreference, isCodeEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; import { Emitter } from 'vs/base/common/event'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -15,11 +15,121 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; -import { Disposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { isEqual } from 'vs/base/common/resources'; import { IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IRange } from 'vs/editor/common/core/range'; +import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { TrackedRangeStickiness, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; + +export interface IRangeHighlightDecoration { + resource: URI; + range: IRange; + isWholeLine?: boolean; +} + +export class RangeHighlightDecorations extends Disposable { + + private readonly _onHighlightRemoved = this._register(new Emitter()); + readonly onHighlightRemoved = this._onHighlightRemoved.event; + + private rangeHighlightDecorationId: string | null = null; + private editor: ICodeEditor | null = null; + private readonly editorDisposables = this._register(new DisposableStore()); + + constructor(@IEditorService private readonly editorService: IEditorService) { + super(); + } + + removeHighlightRange() { + if (this.editor && this.editor.getModel() && this.rangeHighlightDecorationId) { + this.editor.deltaDecorations([this.rangeHighlightDecorationId], []); + this._onHighlightRemoved.fire(); + } + + this.rangeHighlightDecorationId = null; + } + + highlightRange(range: IRangeHighlightDecoration, editor?: any) { + editor = editor ?? this.getEditor(range); + if (isCodeEditor(editor)) { + this.doHighlightRange(editor, range); + } else if (isCompositeEditor(editor) && isCodeEditor(editor.activeCodeEditor)) { + this.doHighlightRange(editor.activeCodeEditor, range); + } + } + + private doHighlightRange(editor: ICodeEditor, selectionRange: IRangeHighlightDecoration) { + this.removeHighlightRange(); + + editor.changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => { + this.rangeHighlightDecorationId = changeAccessor.addDecoration(selectionRange.range, this.createRangeHighlightDecoration(selectionRange.isWholeLine)); + }); + + this.setEditor(editor); + } + + private getEditor(resourceRange: IRangeHighlightDecoration): ICodeEditor | undefined { + const activeEditor = this.editorService.activeEditor; + const resource = activeEditor && activeEditor.resource; + if (resource && isEqual(resource, resourceRange.resource)) { + return this.editorService.activeTextEditorControl as ICodeEditor; + } + + return undefined; + } + + private setEditor(editor: ICodeEditor) { + if (this.editor !== editor) { + this.editorDisposables.clear(); + this.editor = editor; + this.editorDisposables.add(this.editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => { + if ( + e.reason === CursorChangeReason.NotSet + || e.reason === CursorChangeReason.Explicit + || e.reason === CursorChangeReason.Undo + || e.reason === CursorChangeReason.Redo + ) { + this.removeHighlightRange(); + } + })); + this.editorDisposables.add(this.editor.onDidChangeModel(() => { this.removeHighlightRange(); })); + this.editorDisposables.add(this.editor.onDidDispose(() => { + this.removeHighlightRange(); + this.editor = null; + })); + } + } + + private static readonly _WHOLE_LINE_RANGE_HIGHLIGHT = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: 'rangeHighlight', + isWholeLine: true + }); + + private static readonly _RANGE_HIGHLIGHT = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: 'rangeHighlight' + }); + + private createRangeHighlightDecoration(isWholeLine: boolean = true): ModelDecorationOptions { + return (isWholeLine ? RangeHighlightDecorations._WHOLE_LINE_RANGE_HIGHLIGHT : RangeHighlightDecorations._RANGE_HIGHLIGHT); + } + + dispose() { + super.dispose(); + + if (this.editor && this.editor.getModel()) { + this.removeHighlightRange(); + this.editor = null; + } + } +} export class FloatingClickWidget extends Widget implements IOverlayWidget { diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index 16f7cf463..c0087e858 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -56,15 +56,26 @@ export abstract class Composite extends Component implements IComposite { return this._onDidBlur.event; } + private _hasFocus = false; + hasFocus(): boolean { + return this._hasFocus; + } + private registerFocusTrackEvents(): { onDidFocus: Emitter, onDidBlur: Emitter } { const container = assertIsDefined(this.getContainer()); const focusTracker = this._register(trackFocus(container)); const onDidFocus = this._onDidFocus = this._register(new Emitter()); - this._register(focusTracker.onDidFocus(() => onDidFocus.fire())); + this._register(focusTracker.onDidFocus(() => { + this._hasFocus = true; + onDidFocus.fire(); + })); const onDidBlur = this._onDidBlur = this._register(new Emitter()); - this._register(focusTracker.onDidBlur(() => onDidBlur.fire())); + this._register(focusTracker.onDidBlur(() => { + this._hasFocus = false; + onDidBlur.fire(); + })); return { onDidFocus, onDidBlur }; } @@ -234,7 +245,6 @@ export abstract class CompositeDescriptor { readonly cssClass?: string, readonly order?: number, readonly requestedIndex?: number, - readonly keybindingId?: string, ) { } instantiate(instantiationService: IInstantiationService): T { diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 10e7b53ef..833ec2215 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -17,7 +17,7 @@ import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/ import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; import { IWorkbenchLayoutService, Parts, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { PanelPositionContext } from 'vs/workbench/common/panel'; +import { PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from 'vs/workbench/common/panel'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { isNative } from 'vs/base/common/platform'; @@ -64,6 +64,8 @@ export class WorkbenchContextKeysHandler extends Disposable { private sideBarVisibleContext: IContextKey; private editorAreaVisibleContext: IContextKey; private panelPositionContext: IContextKey; + private panelVisibleContext: IContextKey; + private panelMaximizedContext: IContextKey; constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -146,9 +148,13 @@ export class WorkbenchContextKeysHandler extends Disposable { // Sidebar this.sideBarVisibleContext = SideBarVisibleContext.bindTo(this.contextKeyService); - // Panel Position + // Panel this.panelPositionContext = PanelPositionContext.bindTo(this.contextKeyService); this.panelPositionContext.set(positionToString(this.layoutService.getPanelPosition())); + this.panelVisibleContext = PanelVisibleContext.bindTo(this.contextKeyService); + this.panelVisibleContext.set(this.layoutService.isVisible(Parts.PANEL_PART)); + this.panelMaximizedContext = PanelMaximizedContext.bindTo(this.contextKeyService); + this.panelMaximizedContext.set(this.layoutService.isPanelMaximized()); this.registerListeners(); } @@ -182,7 +188,11 @@ export class WorkbenchContextKeysHandler extends Disposable { this._register(this.viewletService.onDidViewletClose(() => this.updateSideBarContextKeys())); this._register(this.viewletService.onDidViewletOpen(() => this.updateSideBarContextKeys())); - this._register(this.layoutService.onPartVisibilityChange(() => this.editorAreaVisibleContext.set(this.layoutService.isVisible(Parts.EDITOR_PART)))); + this._register(this.layoutService.onPartVisibilityChange(() => { + this.editorAreaVisibleContext.set(this.layoutService.isVisible(Parts.EDITOR_PART)); + this.panelVisibleContext.set(this.layoutService.isVisible(Parts.PANEL_PART)); + this.panelMaximizedContext.set(this.layoutService.isPanelMaximized()); + })); this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.dirtyWorkingCopiesContext.set(workingCopy.isDirty() || this.workingCopyService.hasDirty))); } diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index baab13434..962b92b73 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -737,7 +737,7 @@ export class CompositeDragAndDropObserver extends Disposable { if (callbacks.onDragEnd) { this._onDragEnd.event(e => { callbacks.onDragEnd!(e); - }); + }, this, disposableStore); } return this._register(disposableStore); } diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 5df3e69fd..fd865714e 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -12,7 +12,6 @@ import { insert } from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export interface IEditorDescriptor { - getId(): string; getName(): string; diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 5984484d2..a4729a6bf 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -100,6 +100,10 @@ export const DEFAULT_LABELS_CONTAINER: IResourceLabelsContainer = { }; export class ResourceLabels extends Disposable { + + private _onDidChangeDecorations = this._register(new Emitter()); + readonly onDidChangeDecorations = this._onDidChangeDecorations.event; + private widgets: ResourceLabelWidget[] = []; private labels: IResourceLabel[] = []; @@ -148,7 +152,18 @@ export class ResourceLabels extends Disposable { })); // notify when file decoration changes - this._register(this.decorationsService.onDidChangeDecorations(e => this.widgets.forEach(widget => widget.notifyFileDecorationsChanges(e)))); + this._register(this.decorationsService.onDidChangeDecorations(e => { + let notifyDidChangeDecorations = false; + this.widgets.forEach(widget => { + if (widget.notifyFileDecorationsChanges(e)) { + notifyDidChangeDecorations = true; + } + }); + + if (notifyDidChangeDecorations) { + this._onDidChangeDecorations.fire(); + } + })); // notify when theme changes this._register(this.themeService.onDidColorThemeChange(() => this.widgets.forEach(widget => widget.notifyThemeChange()))); @@ -311,19 +326,21 @@ class ResourceLabelWidget extends IconLabel { } } - notifyFileDecorationsChanges(e: IResourceDecorationChangeEvent): void { + notifyFileDecorationsChanges(e: IResourceDecorationChangeEvent): boolean { if (!this.options) { - return; + return false; } const resource = toResource(this.label); if (!resource) { - return; + return false; } if (this.options.fileDecorations && e.affectsResource(resource)) { - this.render(false); + return this.render(false); } + + return false; } notifyExtensionsRegistered(): void { @@ -465,7 +482,7 @@ class ResourceLabelWidget extends IconLabel { this.setLabel(''); } - private render(clearIconCache: boolean): void { + private render(clearIconCache: boolean): boolean { if (this.isHidden) { if (!this.needsRedraw) { this.needsRedraw = clearIconCache ? Redraw.Full : Redraw.Basic; @@ -475,7 +492,7 @@ class ResourceLabelWidget extends IconLabel { this.needsRedraw = Redraw.Full; } - return; + return false; } if (this.label) { @@ -492,7 +509,7 @@ class ResourceLabelWidget extends IconLabel { } if (!this.label) { - return; + return false; } this.renderDisposables.clear(); @@ -558,6 +575,8 @@ class ResourceLabelWidget extends IconLabel { this.setLabel(label || '', this.label.description, iconLabelOptions); this._onDidRender.fire(); + + return true; } dispose(): void { diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 460594ee0..1fe5aa621 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -78,7 +78,9 @@ enum Storage { GRID_LAYOUT = 'workbench.grid.layout', GRID_WIDTH = 'workbench.grid.width', - GRID_HEIGHT = 'workbench.grid.height' + GRID_HEIGHT = 'workbench.grid.height', + + MENU_VISIBILITY = 'window.menuBarVisibility' } enum Classes { @@ -321,9 +323,12 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (this.state.fullscreen && (this.state.menuBar.visibility === 'toggle' || this.state.menuBar.visibility === 'default')) { // Propagate to grid this.workbenchGrid.setViewVisible(this.titleBarPartView, this.isVisible(Parts.TITLEBAR_PART)); - - this.layout(); } + + // Move layout call to any time the menubar + // is toggled to update consumers of offset + // see issue #115267 + this.layout(); } } @@ -622,7 +627,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return this._openedDefaultEditors; } - private getInitialFilesToOpen(): { filesToOpenOrCreate?: IPath[], filesToDiff?: IPath[] } | undefined { + private getInitialFilesToOpen(): { filesToOpenOrCreate?: IPath[], filesToDiff?: IPath[]; } | undefined { const defaultLayout = this.environmentService.options?.defaultLayout; if (defaultLayout?.editors?.length && this.storageService.isNew(StorageScope.WORKSPACE)) { this._openedDefaultEditors = true; @@ -652,7 +657,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Restore editors restorePromises.push((async () => { - mark('willRestoreEditors'); + mark('code/willRestoreEditors'); // first ensure the editor part is restored await this.editorGroupService.whenRestored; @@ -669,17 +674,17 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi await this.editorService.openEditors(editors); } - mark('didRestoreEditors'); + mark('code/didRestoreEditors'); })()); // Restore default views const restoreDefaultViewsPromise = (async () => { if (this.state.views.defaults?.length) { - mark('willOpenDefaultViews'); + mark('code/willOpenDefaultViews'); - let locationsRestored: { id: string; order: number }[] = []; + let locationsRestored: { id: string; order: number; }[] = []; - const tryOpenView = (view: { id: string; order: number }): boolean => { + const tryOpenView = (view: { id: string; order: number; }): boolean => { const location = this.viewDescriptorService.getViewLocationById(view.id); if (location !== null) { const container = this.viewDescriptorService.getViewContainerByViewId(view.id); @@ -733,7 +738,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.state.panel.panelToRestore = locationsRestored[ViewContainerLocation.Panel].id; } - mark('didOpenDefaultViews'); + mark('code/didOpenDefaultViews'); } })(); restorePromises.push(restoreDefaultViewsPromise); @@ -748,14 +753,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return; } - mark('willRestoreViewlet'); + mark('code/willRestoreViewlet'); const viewlet = await this.viewletService.openViewlet(this.state.sideBar.viewletToRestore); if (!viewlet) { await this.viewletService.openViewlet(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id); // fallback to default viewlet as needed } - mark('didRestoreViewlet'); + mark('code/didRestoreViewlet'); })()); // Restore Panel @@ -768,14 +773,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return; } - mark('willRestorePanel'); + mark('code/willRestorePanel'); const panel = await this.panelService.openPanel(this.state.panel.panelToRestore!); if (!panel) { await this.panelService.openPanel(Registry.as(PanelExtensions.Panels).getDefaultPanelId()); // fallback to default panel as needed } - mark('didRestorePanel'); + mark('code/didRestorePanel'); })()); // Restore Zen Mode @@ -1128,7 +1133,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi [Parts.STATUSBAR_PART]: this.statusBarPartView }; - const fromJSON = ({ type }: { type: Parts }) => viewMap[type]; + const fromJSON = ({ type }: { type: Parts; }) => viewMap[type]; const workbenchGrid = SerializableGrid.deserialize( this.createGridDescriptor(), { fromJSON }, @@ -1410,7 +1415,29 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // If panel part becomes visible, show last active panel or default panel else if (!hidden && !this.panelService.getActivePanel()) { - const panelToOpen = this.panelService.getLastActivePanelId(); + let panelToOpen: string | undefined = this.panelService.getLastActivePanelId(); + const hasViews = (id: string): boolean => { + const viewContainer = this.viewDescriptorService.getViewContainerById(id); + if (!viewContainer) { + return false; + } + + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); + if (!viewContainerModel) { + return false; + } + + return viewContainerModel.activeViewDescriptors.length >= 1; + }; + + // verify that the panel we try to open has views before we default to it + // otherwise fall back to any view that has views still refs #111463 + if (!panelToOpen || !hasViews(panelToOpen)) { + panelToOpen = this.viewDescriptorService + .getViewContainersByLocation(ViewContainerLocation.Panel) + .find(viewContainer => hasViews(viewContainer.id))?.id; + } + if (panelToOpen) { const focus = !skipLayout; this.panelService.openPanel(panelToOpen, focus); @@ -1529,6 +1556,24 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return this.state.menuBar.visibility; } + toggleMenuBar(): void { + let currentVisibilityValue = getMenuBarVisibility(this.configurationService); + if (typeof currentVisibilityValue !== 'string') { + currentVisibilityValue = 'default'; + } + + let newVisibilityValue: string; + if (currentVisibilityValue === 'visible' || currentVisibilityValue === 'default') { + newVisibilityValue = 'toggle'; + } else if (currentVisibilityValue === 'compact') { + newVisibilityValue = 'hidden'; + } else { + newVisibilityValue = (isWeb && currentVisibilityValue === 'hidden') ? 'compact' : 'default'; + } + + this.configurationService.updateValue(Storage.MENU_VISIBILITY, newVisibilityValue); + } + getPanelPosition(): Position { return this.state.panel.position; } diff --git a/src/vs/workbench/browser/menuActions.ts b/src/vs/workbench/browser/menuActions.ts new file mode 100644 index 000000000..7aecec66e --- /dev/null +++ b/src/vs/workbench/browser/menuActions.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAction } from 'vs/base/common/actions'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { MenuId, IMenuService, IMenu, SubmenuItemAction, IMenuActionOptions } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; + +class MenuActions extends Disposable { + + private readonly menu: IMenu; + + private _primaryActions: IAction[] = []; + get primaryActions() { return this._primaryActions; } + + private _secondaryActions: IAction[] = []; + get secondaryActions() { return this._secondaryActions; } + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + private disposables = this._register(new DisposableStore()); + + constructor( + menuId: MenuId, + private readonly options: IMenuActionOptions | undefined, + private readonly menuService: IMenuService, + private readonly contextKeyService: IContextKeyService + ) { + super(); + this.menu = this._register(menuService.createMenu(menuId, contextKeyService)); + this._register(this.menu.onDidChange(() => this.updateActions())); + this.updateActions(); + } + + private updateActions(): void { + this.disposables.clear(); + this._primaryActions = []; + this._secondaryActions = []; + this.disposables.add(createAndFillInActionBarActions(this.menu, this.options, { primary: this._primaryActions, secondary: this._secondaryActions })); + this.disposables.add(this.updateSubmenus([...this._primaryActions, ...this._secondaryActions], {})); + this._onDidChange.fire(); + } + + private updateSubmenus(actions: readonly IAction[], submenus: { [id: number]: IMenu }): IDisposable { + const disposables = new DisposableStore(); + for (const action of actions) { + if (action instanceof SubmenuItemAction && !submenus[action.item.submenu.id]) { + const menu = submenus[action.item.submenu.id] = disposables.add(this.menuService.createMenu(action.item.submenu, this.contextKeyService)); + disposables.add(menu.onDidChange(() => this.updateActions())); + disposables.add(this.updateSubmenus(action.actions, submenus)); + } + } + return disposables; + } +} + +export class CompositeMenuActions extends Disposable { + + private readonly menuActions: MenuActions; + private readonly contextMenuActionsDisposable = this._register(new MutableDisposable()); + + private _onDidChange = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + constructor( + menuId: MenuId, + private readonly contextMenuId: MenuId | undefined, + private readonly options: IMenuActionOptions | undefined, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, + ) { + super(); + this.menuActions = this._register(new MenuActions(menuId, this.options, menuService, contextKeyService)); + this._register(this.menuActions.onDidChange(() => this._onDidChange.fire())); + } + + getPrimaryActions(): IAction[] { + return this.menuActions.primaryActions; + } + + getSecondaryActions(): IAction[] { + return this.menuActions.secondaryActions; + } + + getContextMenuActions(): IAction[] { + const actions: IAction[] = []; + if (this.contextMenuId) { + const menu = this.menuService.createMenu(this.contextMenuId, this.contextKeyService); + this.contextMenuActionsDisposable.value = createAndFillInActionBarActions(menu, this.options, { primary: [], secondary: actions }); + menu.dispose(); + } + return actions; + } +} diff --git a/src/vs/workbench/browser/panecomposite.ts b/src/vs/workbench/browser/panecomposite.ts index 5279b52e2..4a8a1144f 100644 --- a/src/vs/workbench/browser/panecomposite.ts +++ b/src/vs/workbench/browser/panecomposite.ts @@ -16,13 +16,9 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { ViewPaneContainer } from './parts/views/viewPaneContainer'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IAction, IActionViewItem, Separator } from 'vs/base/common/actions'; -import { ViewContainerMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; -import { MenuId } from 'vs/platform/actions/common/actions'; export class PaneComposite extends Composite implements IPaneComposite { - private menuActions: ViewContainerMenuActions; - constructor( id: string, protected readonly viewPaneContainer: ViewPaneContainer, @@ -35,8 +31,6 @@ export class PaneComposite extends Composite implements IPaneComposite { @IWorkspaceContextService protected contextService: IWorkspaceContextService ) { super(id, telemetryService, themeService, storageService); - - this.menuActions = this._register(this.instantiationService.createInstance(ViewContainerMenuActions, this.getId(), MenuId.ViewContainerTitleContext)); this._register(this.viewPaneContainer.onTitleAreaUpdate(() => this.updateTitleArea())); } @@ -71,22 +65,36 @@ export class PaneComposite extends Composite implements IPaneComposite { getContextMenuActions(): ReadonlyArray { const result = []; - result.push(...this.menuActions.getContextMenuActions()); + result.push(...this.viewPaneContainer.getContextMenuActions2()); - if (result.length) { + const otherActions = this.viewPaneContainer.getContextMenuActions(); + + if (otherActions.length) { result.push(new Separator()); + result.push(...otherActions); } - result.push(...this.viewPaneContainer.getContextMenuActions()); return result; } getActions(): ReadonlyArray { - return this.viewPaneContainer.getActions(); + const result = []; + result.push(...this.viewPaneContainer.getActions2()); + result.push(...this.viewPaneContainer.getActions()); + return result; } getSecondaryActions(): ReadonlyArray { - return this.viewPaneContainer.getSecondaryActions(); + const menuActions = this.viewPaneContainer.getSecondaryActions2(); + const viewPaneContainerActions = this.viewPaneContainer.getSecondaryActions(); + if (menuActions.length && viewPaneContainerActions.length) { + return [ + ...menuActions, + new Separator(), + ...viewPaneContainerActions + ]; + } + return menuActions.length ? menuActions : viewPaneContainerActions; } getActionViewItem(action: IAction): IActionViewItem | undefined { diff --git a/src/vs/workbench/browser/panel.ts b/src/vs/workbench/browser/panel.ts index b2b2a18b4..0d04a6a1c 100644 --- a/src/vs/workbench/browser/panel.ts +++ b/src/vs/workbench/browser/panel.ts @@ -6,23 +6,74 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IPanel } from 'vs/workbench/common/panel'; import { CompositeDescriptor, CompositeRegistry } from 'vs/workbench/browser/composite'; -import { IConstructorSignature0, BrandedService } from 'vs/platform/instantiation/common/instantiation'; +import { IConstructorSignature0, BrandedService, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { assertIsDefined } from 'vs/base/common/types'; import { PaneComposite } from 'vs/workbench/browser/panecomposite'; +import { IAction, Separator } from 'vs/base/common/actions'; +import { CompositeMenuActions } from 'vs/workbench/browser/menuActions'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -export abstract class Panel extends PaneComposite implements IPanel { } +export abstract class Panel extends PaneComposite implements IPanel { + + private readonly panelActions: CompositeMenuActions; + + constructor(id: string, + viewPaneContainer: ViewPaneContainer, + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IExtensionService extensionService: IExtensionService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + ) { + super(id, viewPaneContainer, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); + this.panelActions = this._register(this.instantiationService.createInstance(CompositeMenuActions, MenuId.PanelTitle, undefined, undefined)); + this._register(this.panelActions.onDidChange(() => this.updateTitleArea())); + } + + getActions(): ReadonlyArray { + return [...super.getActions(), ...this.panelActions.getPrimaryActions()]; + } + + getSecondaryActions(): ReadonlyArray { + return this.mergeSecondaryActions(super.getSecondaryActions(), this.panelActions.getSecondaryActions()); + } + + getContextMenuActions(): ReadonlyArray { + return this.mergeSecondaryActions(super.getContextMenuActions(), this.panelActions.getContextMenuActions()); + } + + private mergeSecondaryActions(actions: ReadonlyArray, panelActions: IAction[]): ReadonlyArray { + if (panelActions.length && actions.length) { + return [ + ...actions, + new Separator(), + ...panelActions, + ]; + } + return panelActions.length ? panelActions : actions; + } +} /** * A panel descriptor is a leightweight descriptor of a panel in the workbench. */ export class PanelDescriptor extends CompositeDescriptor { - static create(ctor: { new(...services: Services): Panel }, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number, _commandId?: string): PanelDescriptor { - return new PanelDescriptor(ctor as IConstructorSignature0, id, name, cssClass, order, requestedIndex, _commandId); + static create(ctor: { new(...services: Services): Panel }, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number): PanelDescriptor { + return new PanelDescriptor(ctor as IConstructorSignature0, id, name, cssClass, order, requestedIndex); } - private constructor(ctor: IConstructorSignature0, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number, _commandId?: string) { - super(ctor, id, name, cssClass, order, requestedIndex, _commandId); + private constructor(ctor: IConstructorSignature0, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number) { + super(ctor, id, name, cssClass, order, requestedIndex); } } diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index dfab3bc5c..8e78cbccc 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -162,7 +162,7 @@ class PartLayout { if (this.options && this.options.hasTitle) { titleSize = new Dimension(width, Math.min(height, PartLayout.TITLE_HEIGHT)); } else { - titleSize = new Dimension(0, 0); + titleSize = Dimension.None; } let contentWidth = width; diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 49bbae451..485e48516 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -4,18 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/activityaction'; -import * as nls from 'vs/nls'; -import * as DOM from 'vs/base/browser/dom'; +import { localize } from 'vs/nls'; +import { EventType, addDisposableListener, EventHelper } from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; -import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; +import { Action, IAction, Separator, SubmenuAction, toAction } from 'vs/base/common/actions'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IMenuService, MenuId, IMenu, registerAction2, Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { activeContrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; -import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ActivityAction, ActivityActionViewItem, ICompositeBar, ICompositeBarColors, ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { IActivity } from 'vs/workbench/common/activity'; @@ -29,12 +29,12 @@ import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { getCurrentAuthenticationSessionInfo, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { AuthenticationSession } from 'vs/editor/common/modes'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IProductService } from 'vs/platform/product/common/productService'; import { AnchorAlignment, AnchorAxisAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { getTitleBarStyle } from 'vs/platform/windows/common/windows'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; export class ViewContainerActivityAction extends ActivityAction { @@ -97,10 +97,10 @@ export class ViewContainerActivityAction extends ActivityAction { private logAction(action: string) { type ActivityBarActionClassification = { - viewletId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - action: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + viewletId: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; + action: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; }; - this.telemetryService.publicLog2<{ viewletId: String, action: String }, ActivityBarActionClassification>('activityBarAction', { viewletId: this.activity.id, action }); + this.telemetryService.publicLog2<{ viewletId: String, action: String; }, ActivityBarActionClassification>('activityBarAction', { viewletId: this.activity.id, action }); } } @@ -109,6 +109,7 @@ class MenuActivityActionViewItem extends ActivityActionViewItem { constructor( private readonly menuId: MenuId, action: ActivityAction, + private contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, @IThemeService themeService: IThemeService, @IMenuService protected readonly menuService: IMenuService, @@ -126,30 +127,35 @@ class MenuActivityActionViewItem extends ActivityActionViewItem { // Context menus are triggered on mouse down so that an item can be picked // and executed with releasing the mouse over it - this._register(DOM.addDisposableListener(this.container, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => { - DOM.EventHelper.stop(e, true); + this._register(addDisposableListener(this.container, EventType.MOUSE_DOWN, (e: MouseEvent) => { + EventHelper.stop(e, true); this.showContextMenu(e); })); - this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, (e: KeyboardEvent) => { + this._register(addDisposableListener(this.container, EventType.KEY_UP, (e: KeyboardEvent) => { let event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { - DOM.EventHelper.stop(e, true); + EventHelper.stop(e, true); this.showContextMenu(); } })); - this._register(DOM.addDisposableListener(this.container, TouchEventType.Tap, (e: GestureEvent) => { - DOM.EventHelper.stop(e, true); + this._register(addDisposableListener(this.container, TouchEventType.Tap, (e: GestureEvent) => { + EventHelper.stop(e, true); this.showContextMenu(); })); } - protected async showContextMenu(e?: MouseEvent): Promise { + private async showContextMenu(e?: MouseEvent): Promise { const disposables = new DisposableStore(); - const menu = disposables.add(this.menuService.createMenu(this.menuId, this.contextKeyService)); - const actions = await this.resolveActions(menu, disposables); + let actions: IAction[]; + if (e?.button !== 2) { + const menu = disposables.add(this.menuService.createMenu(this.menuId, this.contextKeyService)); + actions = await this.resolveMainMenuActions(menu, disposables); + } else { + actions = await this.resolveContextMenuActions(disposables); + } const isUsingCustomMenu = isWeb || (getTitleBarStyle(this.configurationService) !== 'native' && !isMacintosh); // see #40262 const position = this.configurationService.getValue('workbench.sideBar.location'); @@ -163,13 +169,17 @@ class MenuActivityActionViewItem extends ActivityActionViewItem { }); } - protected async resolveActions(menu: IMenu, disposables: DisposableStore): Promise { + protected async resolveMainMenuActions(menu: IMenu, disposables: DisposableStore): Promise { const actions: IAction[] = []; disposables.add(createAndFillInActionBarActions(menu, undefined, { primary: [], secondary: actions })); return actions; } + + protected async resolveContextMenuActions(disposables: DisposableStore): Promise { + return this.contextMenuActionsProvider(); + } } export class HomeActivityActionViewItem extends MenuActivityActionViewItem { @@ -179,6 +189,7 @@ export class HomeActivityActionViewItem extends MenuActivityActionViewItem { constructor( private readonly goHomeHref: string, action: ActivityAction, + contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, @IThemeService themeService: IThemeService, @IMenuService menuService: IMenuService, @@ -188,27 +199,32 @@ export class HomeActivityActionViewItem extends MenuActivityActionViewItem { @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IStorageService private readonly storageService: IStorageService ) { - super(MenuId.MenubarHomeMenu, action, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); + super(MenuId.MenubarHomeMenu, action, contextMenuActionsProvider, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); } - protected async resolveActions(homeMenu: IMenu, disposables: DisposableStore): Promise { + protected async resolveMainMenuActions(homeMenu: IMenu, disposables: DisposableStore): Promise { const actions = []; // Go Home - actions.push(disposables.add(new Action('goHome', nls.localize('goHome', "Go Home"), undefined, true, async () => window.location.href = this.goHomeHref))); - actions.push(disposables.add(new Separator())); + actions.push(toAction({ id: 'goHome', label: localize('goHome', "Go Home"), run: () => window.location.href = this.goHomeHref })); // Contributed - const contributedActions = await super.resolveActions(homeMenu, disposables); - actions.push(...contributedActions); - - // Hide - if (contributedActions.length > 0) { + const contributedActions = await super.resolveMainMenuActions(homeMenu, disposables); + if (contributedActions.length) { actions.push(disposables.add(new Separator())); + actions.push(...contributedActions); } - actions.push(disposables.add(new Action('hide', nls.localize('hide', "Hide"), undefined, true, async () => { - this.storageService.store(HomeActivityActionViewItem.HOME_BAR_VISIBILITY_PREFERENCE, false, StorageScope.GLOBAL, StorageTarget.USER); - }))); + + return actions; + } + + protected async resolveContextMenuActions(disposables: DisposableStore): Promise { + const actions = await super.resolveContextMenuActions(disposables); + + actions.unshift(...[ + toAction({ id: 'hideHomeButton', label: localize('hideHomeButton', "Hide Home Button"), run: () => this.storageService.store(HomeActivityActionViewItem.HOME_BAR_VISIBILITY_PREFERENCE, false, StorageScope.GLOBAL, StorageTarget.USER) }), + new Separator() + ]); return actions; } @@ -220,6 +236,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { constructor( action: ActivityAction, + contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, @IThemeService themeService: IThemeService, @IContextMenuService contextMenuService: IContextMenuService, @@ -227,15 +244,15 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { @IContextKeyService contextKeyService: IContextKeyService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IStorageService private readonly storageService: IStorageService, @IProductService private readonly productService: IProductService, @IConfigurationService configurationService: IConfigurationService, + @IStorageService private readonly storageService: IStorageService ) { - super(MenuId.AccountsContext, action, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); + super(MenuId.AccountsContext, action, contextMenuActionsProvider, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); } - protected async resolveActions(accountsMenu: IMenu, disposables: DisposableStore): Promise { - await super.resolveActions(accountsMenu, disposables); + protected async resolveMainMenuActions(accountsMenu: IMenu, disposables: DisposableStore): Promise { + await super.resolveMainMenuActions(accountsMenu, disposables); const otherCommands = accountsMenu.getActions(); const providers = this.authenticationService.getProviderIds(); @@ -243,7 +260,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { try { const sessions = await this.authenticationService.getSessions(providerId); - const groupedSessions: { [label: string]: AuthenticationSession[] } = {}; + const groupedSessions: { [label: string]: AuthenticationSession[]; } = {}; sessions.forEach(session => { if (groupedSessions[session.account.label]) { groupedSessions[session.account.label].push(session); @@ -266,11 +283,11 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { if (sessionInfo.sessions) { Object.keys(sessionInfo.sessions).forEach(accountName => { - const manageExtensionsAction = disposables.add(new Action(`configureSessions${accountName}`, nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"), '', true, () => { + const manageExtensionsAction = disposables.add(new Action(`configureSessions${accountName}`, localize('manageTrustedExtensions', "Manage Trusted Extensions"), '', true, () => { return this.authenticationService.manageTrustedExtensionsForAccount(sessionInfo.providerId, accountName); })); - const signOutAction = disposables.add(new Action('signOut', nls.localize('signOut', "Sign Out"), '', true, () => { + const signOutAction = disposables.add(new Action('signOut', localize('signOut', "Sign Out"), '', true, () => { return this.authenticationService.signOutOfAccount(sessionInfo.providerId, accountName); })); @@ -285,7 +302,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { menus.push(providerSubMenu); }); } else { - const providerUnavailableAction = disposables.add(new Action('providerUnavailable', nls.localize('authProviderUnavailable', '{0} is currently unavailable', providerDisplayName))); + const providerUnavailableAction = disposables.add(new Action('providerUnavailable', localize('authProviderUnavailable', '{0} is currently unavailable', providerDisplayName))); menus.push(providerUnavailableAction); } }); @@ -302,22 +319,26 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { } }); - if (menus.length) { - menus.push(disposables.add(new Separator())); - } - - menus.push(disposables.add(new Action('hide', nls.localize('hide', "Hide"), undefined, true, async () => { - this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, false, StorageScope.GLOBAL, StorageTarget.USER); - }))); - return menus; } + + protected async resolveContextMenuActions(disposables: DisposableStore): Promise { + const actions = await super.resolveContextMenuActions(disposables); + + actions.unshift(...[ + toAction({ id: 'hideAccounts', label: localize('hideAccounts', "Hide Accounts"), run: () => this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, false, StorageScope.GLOBAL, StorageTarget.USER) }), + new Separator() + ]); + + return actions; + } } export class GlobalActivityActionViewItem extends MenuActivityActionViewItem { constructor( action: ActivityAction, + contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, @IThemeService themeService: IThemeService, @IMenuService menuService: IMenuService, @@ -326,7 +347,7 @@ export class GlobalActivityActionViewItem extends MenuActivityActionViewItem { @IConfigurationService configurationService: IConfigurationService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - super(MenuId.GlobalActivity, action, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); + super(MenuId.GlobalActivity, action, contextMenuActionsProvider, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); } } @@ -379,7 +400,7 @@ registerAction2( constructor() { super({ id: 'workbench.action.previousSideBarView', - title: { value: nls.localize('previousSideBarView', "Previous Side Bar View"), original: 'Previous Side Bar View' }, + title: { value: localize('previousSideBarView', "Previous Side Bar View"), original: 'Previous Side Bar View' }, category: CATEGORIES.View, f1: true }, -1); @@ -392,7 +413,7 @@ registerAction2( constructor() { super({ id: 'workbench.action.nextSideBarView', - title: { value: nls.localize('nextSideBarView', "Next Side Bar View"), original: 'Next Side Bar View' }, + title: { value: localize('nextSideBarView', "Next Side Bar View"), original: 'Next Side Bar View' }, category: CATEGORIES.View, f1: true }, 1); @@ -400,7 +421,7 @@ registerAction2( } ); -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { const activityBarBackgroundColor = theme.getColor(ACTIVITY_BAR_BACKGROUND); if (activityBarBackgroundColor) { collector.addRule(` diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 39a3ca76e..4e78ba227 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/activitybarpart'; -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { GLOBAL_ACTIVITY_ID, IActivity, ACCOUNTS_ACTIVITY_ID } from 'vs/workbench/common/activity'; import { Part } from 'vs/workbench/browser/part'; @@ -13,7 +13,7 @@ import { IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; -import { ToggleActivityBarVisibilityAction, ToggleMenuBarAction, ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; +import { ToggleActivityBarVisibilityAction, ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; import { IThemeService, IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_ACTIVE_BACKGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BORDER } from 'vs/workbench/common/theme'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; @@ -23,33 +23,34 @@ import { IStorageService, StorageScope, IStorageValueChangeEvent, StorageTarget import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ToggleCompositePinnedAction, ICompositeBarColors, ActivityAction, ICompositeActivity } from 'vs/workbench/browser/parts/compositeBarActions'; -import { IViewDescriptorService, ViewContainer, TEST_VIEW_CONTAINER_ID, IViewContainerModel, ViewContainerLocation, IViewsService } from 'vs/workbench/common/views'; +import { IViewDescriptorService, ViewContainer, IViewContainerModel, ViewContainerLocation, IViewsService } from 'vs/workbench/common/views'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { assertIsDefined } from 'vs/base/common/types'; +import { assertIsDefined, isString } from 'vs/base/common/types'; import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { Schemas } from 'vs/base/common/network'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { getMenuBarVisibility } from 'vs/platform/windows/common/windows'; -import { isWeb } from 'vs/base/common/platform'; +import { isNative, isWeb } from 'vs/base/common/platform'; import { Before2D } from 'vs/workbench/browser/dnd'; import { Codicon, iconRegistry } from 'vs/base/common/codicons'; -import { Action, Separator } from 'vs/base/common/actions'; +import { IAction, Separator, toAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { StringSHA1 } from 'vs/base/common/hash'; interface IPlaceholderViewContainer { readonly id: string; readonly name?: string; readonly iconUrl?: UriComponents; readonly themeIcon?: ThemeIcon; - readonly views?: { when?: string }[]; + readonly isBuiltin?: boolean; + readonly views?: { when?: string; }[]; } interface IPinnedViewContainer { @@ -66,12 +67,10 @@ interface ICachedViewContainer { readonly pinned: boolean; readonly order?: number; visible: boolean; - views?: { when?: string }[]; + isBuiltin?: boolean; + views?: { when?: string; }[]; } -const settingsViewBarIcon = registerIcon('settings-view-bar-icon', Codicon.settingsGear, nls.localize('settingsViewBarIcon', 'Settings icon in the view bar.')); -const accountsViewBarIcon = registerIcon('accounts-view-bar-icon', Codicon.account, nls.localize('accountsViewBarIcon', 'Accounts icon in the view bar.')); - export class ActivitybarPart extends Part implements IActivityBarService { declare readonly _serviceBrand: undefined; @@ -81,6 +80,9 @@ export class ActivitybarPart extends Part implements IActivityBarService { private static readonly ACTION_HEIGHT = 48; private static readonly ACCOUNTS_ACTION_INDEX = 0; + private static readonly GEAR_ICON = registerIcon('settings-view-bar-icon', Codicon.settingsGear, localize('settingsViewBarIcon', "Settings icon in the view bar.")); + private static readonly ACCOUNTS_ICON = registerIcon('accounts-view-bar-icon', Codicon.account, localize('accountsViewBarIcon', "Accounts icon in the view bar.")); + //#region IView readonly minimumWidth: number = 48; @@ -110,12 +112,13 @@ export class ActivitybarPart extends Part implements IActivityBarService { private readonly accountsActivity: ICompositeActivity[] = []; - private readonly compositeActions = new Map(); + private readonly compositeActions = new Map(); private readonly viewContainerDisposables = new Map(); private readonly keyboardNavigationDisposables = this._register(new DisposableStore()); private readonly location = ViewContainerLocation.Sidebar; + private hasExtensionsRegistered: boolean = false; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -132,14 +135,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { super(Parts.ACTIVITYBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); for (const cachedViewContainer of this.cachedViewContainers) { - if ( - environmentService.remoteAuthority || // In remote window, hide activity bar entries until registered - this.shouldBeHidden(cachedViewContainer.id, cachedViewContainer) - ) { - cachedViewContainer.visible = false; - } + cachedViewContainer.visible = !this.shouldBeHidden(cachedViewContainer.id, cachedViewContainer); } - this.compositeBar = this.createCompositeBar(); this.onDidRegisterViewContainers(this.getViewContainers()); @@ -161,53 +158,66 @@ export class ActivitybarPart extends Part implements IActivityBarService { icon: true, orientation: ActionsOrientation.VERTICAL, preventLoopNavigation: true, - openComposite: (compositeId: string) => this.viewsService.openViewContainer(compositeId, true), - getActivityAction: (compositeId: string) => this.getCompositeActions(compositeId).activityAction, - getCompositePinnedAction: (compositeId: string) => this.getCompositeActions(compositeId).pinnedAction, - getOnCompositeClickAction: (compositeId: string) => new Action(compositeId, '', '', true, () => this.viewsService.isViewContainerVisible(compositeId) ? Promise.resolve(this.viewsService.closeViewContainer(compositeId)) : this.viewsService.openViewContainer(compositeId)), - getContextMenuActions: () => { - const actions = []; + openComposite: compositeId => this.viewsService.openViewContainer(compositeId, true), + getActivityAction: compositeId => this.getCompositeActions(compositeId).activityAction, + getCompositePinnedAction: compositeId => this.getCompositeActions(compositeId).pinnedAction, + getOnCompositeClickAction: compositeId => toAction({ id: compositeId, label: '', run: async () => this.viewsService.isViewContainerVisible(compositeId) ? this.viewsService.closeViewContainer(compositeId) : this.viewsService.openViewContainer(compositeId) }), + fillExtraContextMenuActions: actions => { // Home + const topActions: IAction[] = []; if (this.homeBarContainer) { - actions.push(new Action( - 'toggleHomeBarAction', - this.homeBarVisibilityPreference ? nls.localize('hideHomeBar', "Hide Home Button") : nls.localize('showHomeBar', "Show Home Button"), - undefined, - true, - async () => { this.homeBarVisibilityPreference = !this.homeBarVisibilityPreference; } - )); + topActions.push({ + id: 'toggleHomeBarAction', + label: localize('homeButton', "Home Button"), + class: undefined, + tooltip: localize('homeButton', "Home Button"), + checked: this.homeBarVisibilityPreference, + enabled: true, + run: async () => this.homeBarVisibilityPreference = !this.homeBarVisibilityPreference, + dispose: () => { } + }); } // Menu const menuBarVisibility = getMenuBarVisibility(this.configurationService); if (menuBarVisibility === 'compact' || (menuBarVisibility === 'hidden' && isWeb)) { - actions.push(this.instantiationService.createInstance(ToggleMenuBarAction, ToggleMenuBarAction.ID, menuBarVisibility === 'compact' ? nls.localize('hideMenu', "Hide Menu") : nls.localize('showMenu', "Show Menu"))); + topActions.push({ + id: 'toggleMenuVisibility', + label: localize('menu', "Menu"), + class: undefined, + tooltip: localize('menu', "Menu"), + checked: menuBarVisibility === 'compact', + enabled: true, + run: async () => this.layoutService.toggleMenuBar(), + dispose: () => { } + }); + } + + if (topActions.length) { + actions.unshift(...topActions, new Separator()); } // Accounts - actions.push(new Action( - 'toggleAccountsVisibility', - this.accountsVisibilityPreference ? nls.localize('hideAccounts', "Hide Accounts") : nls.localize('showAccounts', "Show Accounts"), - undefined, - true, - async () => { this.accountsVisibilityPreference = !this.accountsVisibilityPreference; } - )); + actions.push(new Separator()); + actions.push({ + id: 'toggleAccountsVisibility', + label: localize('accounts', "Accounts"), + class: undefined, + tooltip: localize('accounts', "Accounts"), + checked: this.accountsVisibilityPreference, + enabled: true, + run: async () => this.accountsVisibilityPreference = !this.accountsVisibilityPreference, + dispose: () => { } + }); + actions.push(new Separator()); // Toggle Sidebar actions.push(this.instantiationService.createInstance(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.getLabel(this.layoutService))); // Toggle Activity Bar - actions.push(new Action( - ToggleActivityBarVisibilityAction.ID, - nls.localize('hideActivitBar', "Hide Activity Bar"), - undefined, - true, - async () => { this.instantiationService.invokeFunction(accessor => new ToggleActivityBarVisibilityAction().run(accessor)); } - )); - - return actions; + actions.push(toAction({ id: ToggleActivityBarVisibilityAction.ID, label: localize('hideActivitBar', "Hide Activity Bar"), run: async () => this.instantiationService.invokeFunction(accessor => new ToggleActivityBarVisibilityAction().run(accessor)) })); }, getContextMenuActionsForComposite: compositeId => this.getContextMenuActionsForComposite(compositeId), getDefaultCompositeId: () => this.viewDescriptorService.getDefaultViewContainer(this.location)!.id, @@ -223,24 +233,20 @@ export class ActivitybarPart extends Part implements IActivityBarService { })); } - private getContextMenuActionsForComposite(compositeId: string): Action[] { - const actions = []; + private getContextMenuActionsForComposite(compositeId: string): IAction[] { + const actions: IAction[] = []; const viewContainer = this.viewDescriptorService.getViewContainerById(compositeId)!; const defaultLocation = this.viewDescriptorService.getDefaultViewContainerLocation(viewContainer)!; if (defaultLocation !== this.viewDescriptorService.getViewContainerLocation(viewContainer)) { - actions.push(new Action('resetLocationAction', nls.localize('resetLocation', "Reset Location"), undefined, true, async () => { - this.viewDescriptorService.moveViewContainerToLocation(viewContainer, defaultLocation); - })); + actions.push(toAction({ id: 'resetLocationAction', label: localize('resetLocation', "Reset Location"), run: () => this.viewDescriptorService.moveViewContainerToLocation(viewContainer, defaultLocation) })); } else { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); if (viewContainerModel.allViewDescriptors.length === 1) { const viewToReset = viewContainerModel.allViewDescriptors[0]; const defaultContainer = this.viewDescriptorService.getDefaultContainerById(viewToReset.id)!; if (defaultContainer !== viewContainer) { - actions.push(new Action('resetLocationAction', nls.localize('resetLocation', "Reset Location"), undefined, true, async () => { - this.viewDescriptorService.moveViewsToContainer([viewToReset], defaultContainer); - })); + actions.push(toAction({ id: 'resetLocationAction', label: localize('resetLocation', "Reset Location"), run: () => this.viewDescriptorService.moveViewsToContainer([viewToReset], defaultContainer) })); } } } @@ -278,7 +284,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { })); } - private onDidChangeViewContainers(added: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation }>, removed: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation }>) { + private onDidChangeViewContainers(added: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation; }>, removed: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation; }>) { removed.filter(({ location }) => location === ViewContainerLocation.Sidebar).forEach(({ container }) => this.onDidDeregisterViewContainer(container)); this.onDidRegisterViewContainers(added.filter(({ location }) => location === ViewContainerLocation.Sidebar).map(({ container }) => container)); } @@ -310,7 +316,22 @@ export class ActivitybarPart extends Part implements IActivityBarService { } private onDidRegisterExtensions(): void { - this.removeNotExistingComposites(); + this.hasExtensionsRegistered = true; + + // show/hide/remove composites + for (const { id } of this.cachedViewContainers) { + const viewContainer = this.getViewContainer(id); + if (viewContainer) { + this.showOrHideViewContainer(viewContainer); + } else { + if (this.viewDescriptorService.isViewContainerRemovedPermanently(id)) { + this.removeComposite(id); + } else { + this.hideComposite(id); + } + } + } + this.saveCachedViewContainers(); } @@ -322,7 +343,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.compositeBar.addComposite(viewContainer); this.compositeBar.activateComposite(viewContainer.id); - if (viewContainer.hideIfEmpty) { + if (this.shouldBeHidden(viewContainer)) { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); if (viewContainerModel.activeViewDescriptors.length === 0) { // Update the composite bar by hiding @@ -460,7 +481,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { // Home action bar const homeIndicator = this.environmentService.options?.homeIndicator; - if (homeIndicator) { + // TODO @sbatten remove the fake setting and associated code + if (homeIndicator && this.configurationService.getValue('window.showHomeIndicator')) { let codicon = iconRegistry.get(homeIndicator.icon); if (!codicon) { codicon = Codicon.code; @@ -556,14 +578,14 @@ export class ActivitybarPart extends Part implements IActivityBarService { private createHomeBar(href: string, icon: Codicon): void { this.homeBarContainer = document.createElement('div'); - this.homeBarContainer.setAttribute('aria-label', nls.localize('homeIndicator', "Home")); + this.homeBarContainer.setAttribute('aria-label', localize('homeIndicator', "Home")); this.homeBarContainer.setAttribute('role', 'toolbar'); this.homeBarContainer.classList.add('home-bar'); this.homeBar = this._register(new ActionBar(this.homeBarContainer, { - actionViewItemProvider: action => this.instantiationService.createInstance(HomeActivityActionViewItem, href, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme)), + actionViewItemProvider: action => this.instantiationService.createInstance(HomeActivityActionViewItem, href, action as ActivityAction, () => this.compositeBar.getContextMenuActions(), (theme: IColorTheme) => this.getActivitybarItemColors(theme)), orientation: ActionsOrientation.VERTICAL, - ariaLabel: nls.localize('home', "Home"), + ariaLabel: localize('home', "Home"), animated: false, preventLoopNavigation: true, ignoreOrientationForPreviousAndNextKey: true @@ -575,7 +597,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.homeBar.push(this._register(new ActivityAction({ id: 'workbench.actions.home', - name: nls.localize('home', "Home"), + name: localize('home', "Home"), cssClass: icon.classNames }))); @@ -587,17 +609,17 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.globalActivityActionBar = this._register(new ActionBar(container, { actionViewItemProvider: action => { if (action.id === 'workbench.actions.manage') { - return this.instantiationService.createInstance(GlobalActivityActionViewItem, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme)); + return this.instantiationService.createInstance(GlobalActivityActionViewItem, action as ActivityAction, () => this.compositeBar.getContextMenuActions(), (theme: IColorTheme) => this.getActivitybarItemColors(theme)); } if (action.id === 'workbench.actions.accounts') { - return this.instantiationService.createInstance(AccountsActivityActionViewItem, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme)); + return this.instantiationService.createInstance(AccountsActivityActionViewItem, action as ActivityAction, () => this.compositeBar.getContextMenuActions(), (theme: IColorTheme) => this.getActivitybarItemColors(theme)); } throw new Error(`No view item for action '${action.id}'`); }, orientation: ActionsOrientation.VERTICAL, - ariaLabel: nls.localize('manage', "Manage"), + ariaLabel: localize('manage', "Manage"), animated: false, preventLoopNavigation: true, ignoreOrientationForPreviousAndNextKey: true @@ -605,15 +627,15 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.globalActivityAction = this._register(new ActivityAction({ id: 'workbench.actions.manage', - name: nls.localize('manage', "Manage"), - cssClass: ThemeIcon.asClassName(settingsViewBarIcon) + name: localize('manage', "Manage"), + cssClass: ThemeIcon.asClassName(ActivitybarPart.GEAR_ICON) })); if (this.accountsVisibilityPreference) { this.accountsActivityAction = this._register(new ActivityAction({ id: 'workbench.actions.accounts', - name: nls.localize('accounts', "Accounts"), - cssClass: ThemeIcon.asClassName(accountsViewBarIcon) + name: localize('accounts', "Accounts"), + cssClass: ThemeIcon.asClassName(ActivitybarPart.ACCOUNTS_ICON) })); this.globalActivityActionBar.push(this.accountsActivityAction, { index: ActivitybarPart.ACCOUNTS_ACTION_INDEX }); @@ -630,7 +652,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { } else { this.accountsActivityAction = this._register(new ActivityAction({ id: 'workbench.actions.accounts', - name: nls.localize('accounts', "Accounts"), + name: localize('accounts', "Accounts"), cssClass: Codicon.account.classNames })); this.globalActivityActionBar.push(this.accountsActivityAction, { index: ActivitybarPart.ACCOUNTS_ACTION_INDEX }); @@ -640,7 +662,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.updateGlobalActivity(ACCOUNTS_ACTIVITY_ID); } - private getCompositeActions(compositeId: string): { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction } { + private getCompositeActions(compositeId: string): { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction; } { let compositeActions = this.compositeActions.get(compositeId); if (!compositeActions) { const viewContainer = this.getViewContainer(compositeId); @@ -666,32 +688,27 @@ export class ActivitybarPart extends Part implements IActivityBarService { private onDidRegisterViewContainers(viewContainers: ReadonlyArray): void { for (const viewContainer of viewContainers) { + this.compositeBar.addComposite(viewContainer); + + // Pin it by default if it is new const cachedViewContainer = this.cachedViewContainers.filter(({ id }) => id === viewContainer.id)[0]; - const visibleViewContainer = this.viewsService.getVisibleViewContainer(this.location); - const isActive = visibleViewContainer?.id === viewContainer.id; - - if (isActive || !this.shouldBeHidden(viewContainer.id, cachedViewContainer)) { - this.compositeBar.addComposite(viewContainer); - - // Pin it by default if it is new - if (!cachedViewContainer) { - this.compositeBar.pin(viewContainer.id); - } - - if (isActive) { - this.compositeBar.activateComposite(viewContainer.id); - } + if (!cachedViewContainer) { + this.compositeBar.pin(viewContainer.id); + } + + // Active + const visibleViewContainer = this.viewsService.getVisibleViewContainer(this.location); + if (visibleViewContainer?.id === viewContainer.id) { + this.compositeBar.activateComposite(viewContainer.id); } - } - for (const viewContainer of viewContainers) { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); this.updateActivity(viewContainer, viewContainerModel); - this.onDidChangeActiveViews(viewContainer, viewContainerModel); + this.showOrHideViewContainer(viewContainer); const disposables = new DisposableStore(); disposables.add(viewContainerModel.onDidChangeContainerInfo(() => this.updateActivity(viewContainer, viewContainerModel))); - disposables.add(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.onDidChangeActiveViews(viewContainer, viewContainerModel))); + disposables.add(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.showOrHideViewContainer(viewContainer))); this.viewContainerDisposables.set(viewContainer.id, disposables); } @@ -728,12 +745,15 @@ export class ActivitybarPart extends Part implements IActivityBarService { let iconUrl: URI | undefined = undefined; if (URI.isUri(icon)) { iconUrl = icon; - cssClass = `activity-${id.replace(/\./g, '-')}`; + const cssUrl = asCSSUrl(icon); + const hash = new StringSHA1(); + hash.update(cssUrl); + cssClass = `activity-${id.replace(/\./g, '-')}-${hash.digest()}`; const iconClass = `.monaco-workbench .activitybar .monaco-action-bar .action-label.${cssClass}`; createCSSRule(iconClass, ` - mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + mask: ${cssUrl} no-repeat 50% 50%; mask-size: 24px; - -webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + -webkit-mask: ${cssUrl} no-repeat 50% 50%; -webkit-mask-size: 24px; `); } else if (ThemeIcon.isThemeIcon(icon)) { @@ -743,36 +763,43 @@ export class ActivitybarPart extends Part implements IActivityBarService { return { id, name, cssClass, iconUrl, keybindingId }; } - private onDidChangeActiveViews(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { - if (viewContainerModel.activeViewDescriptors.length) { - this.compositeBar.addComposite(viewContainer); - } else if (viewContainer.hideIfEmpty) { + private showOrHideViewContainer(viewContainer: ViewContainer): void { + if (this.shouldBeHidden(viewContainer)) { this.hideComposite(viewContainer.id); + } else { + this.compositeBar.addComposite(viewContainer); } } - private shouldBeHidden(viewContainerId: string, cachedViewContainer?: ICachedViewContainer): boolean { - const viewContainer = this.getViewContainer(viewContainerId); - if (!viewContainer || !viewContainer.hideIfEmpty) { - return false; - } + private shouldBeHidden(viewContainerOrId: string | ViewContainer, cachedViewContainer?: ICachedViewContainer): boolean { + const viewContainer = isString(viewContainerOrId) ? this.getViewContainer(viewContainerOrId) : viewContainerOrId; + const viewContainerId = isString(viewContainerOrId) ? viewContainerOrId : viewContainerOrId.id; - return cachedViewContainer?.views && cachedViewContainer.views.length - ? cachedViewContainer.views.every(({ when }) => !!when && !this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(when))) - : viewContainerId === TEST_VIEW_CONTAINER_ID /* Hide Test view container for the first time or it had no views registered before */; - } - - private removeNotExistingComposites(): void { - const viewContainers = this.getViewContainers(); - for (const { id } of this.cachedViewContainers) { - if (viewContainers.every(viewContainer => viewContainer.id !== id)) { - if (this.viewDescriptorService.isViewContainerRemovedPermanently(id)) { - this.removeComposite(id); - } else { - this.hideComposite(id); + if (viewContainer) { + if (viewContainer.hideIfEmpty) { + if (this.viewDescriptorService.getViewContainerModel(viewContainer).activeViewDescriptors.length > 0) { + return false; } + } else { + return false; } } + + // Check cache only if extensions are not yet registered and current window is not native (desktop) remote connection window + if (!this.hasExtensionsRegistered && !(this.environmentService.remoteAuthority && isNative)) { + cachedViewContainer = cachedViewContainer || this.cachedViewContainers.find(({ id }) => id === viewContainerId); + + // Show builtin ViewContainer if not registered yet + if (!viewContainer && cachedViewContainer?.isBuiltin) { + return false; + } + + if (cachedViewContainer?.views?.length) { + return cachedViewContainer.views.every(({ when }) => !!when && !this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(when))); + } + } + + return true; } private hideComposite(compositeId: string): void { @@ -864,7 +891,6 @@ export class ActivitybarPart extends Part implements IActivityBarService { private getViewContainer(id: string): ViewContainer | undefined { const viewContainer = this.viewDescriptorService.getViewContainerById(id); - return viewContainer && this.viewDescriptorService.getViewContainerLocation(viewContainer) === this.location ? viewContainer : undefined; } @@ -918,22 +944,22 @@ export class ActivitybarPart extends Part implements IActivityBarService { const viewContainer = this.getViewContainer(compositeItem.id); if (viewContainer) { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); - const views: { when: string | undefined }[] = []; + const views: { when: string | undefined; }[] = []; for (const { when } of viewContainerModel.allViewDescriptors) { views.push({ when: when ? when.serialize() : undefined }); } - const cacheIcon = URI.isUri(viewContainerModel.icon) ? viewContainerModel.icon.scheme === Schemas.file : true; state.push({ id: compositeItem.id, name: viewContainerModel.title, - icon: cacheIcon ? viewContainerModel.icon : undefined, + icon: URI.isUri(viewContainerModel.icon) && this.environmentService.remoteAuthority && isNative ? undefined : viewContainerModel.icon, /* Donot cache uri icons in desktop with remote connection */ views, pinned: compositeItem.pinned, order: compositeItem.order, - visible: compositeItem.visible + visible: compositeItem.visible, + isBuiltin: !viewContainer.extensionId }); } else { - state.push({ id: compositeItem.id, pinned: compositeItem.pinned, order: compositeItem.order, visible: false }); + state.push({ id: compositeItem.id, pinned: compositeItem.pinned, order: compositeItem.order, visible: false, isBuiltin: false }); } } @@ -948,9 +974,10 @@ export class ActivitybarPart extends Part implements IActivityBarService { const cachedViewContainer = this._cachedViewContainers.filter(cached => cached.id === placeholderViewContainer.id)[0]; if (cachedViewContainer) { cachedViewContainer.name = placeholderViewContainer.name; - cachedViewContainer.icon = placeholderViewContainer.themeIcon ? ThemeIcon.revive(placeholderViewContainer.themeIcon) : + cachedViewContainer.icon = placeholderViewContainer.themeIcon ? placeholderViewContainer.themeIcon : placeholderViewContainer.iconUrl ? URI.revive(placeholderViewContainer.iconUrl) : undefined; cachedViewContainer.views = placeholderViewContainer.views; + cachedViewContainer.isBuiltin = placeholderViewContainer.isBuiltin; } } } @@ -966,11 +993,12 @@ export class ActivitybarPart extends Part implements IActivityBarService { order }))); - this.setPlaceholderViewContainers(cachedViewContainers.map(({ id, icon, name, views }) => ({ + this.setPlaceholderViewContainers(cachedViewContainers.map(({ id, icon, name, views, isBuiltin }) => ({ id, iconUrl: URI.isUri(icon) ? icon : undefined, themeIcon: ThemeIcon.isThemeIcon(icon) ? icon : undefined, name, + isBuiltin, views }))); } @@ -1067,7 +1095,7 @@ class FocusActivityBarAction extends Action2 { constructor() { super({ id: 'workbench.action.focusActivityBar', - title: { value: nls.localize('focusActivityBar', "Focus Activity Bar"), original: 'Focus Activity Bar' }, + title: { value: localize('focusActivityBar', "Focus Activity Bar"), original: 'Focus Activity Bar' }, category: CATEGORIES.View, f1: true }); diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 06a031a56..70d629fb2 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { Action, IAction, Separator } from 'vs/base/common/actions'; +import { IAction, toAction } from 'vs/base/common/actions'; import { illegalArgument } from 'vs/base/common/errors'; import * as arrays from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -149,10 +149,10 @@ export interface ICompositeBarOptions { readonly preventLoopNavigation?: boolean; getActivityAction: (compositeId: string) => ActivityAction; - getCompositePinnedAction: (compositeId: string) => Action; - getOnCompositeClickAction: (compositeId: string) => Action; - getContextMenuActions: () => Action[]; - getContextMenuActionsForComposite: (compositeId: string) => Action[]; + getCompositePinnedAction: (compositeId: string) => IAction; + getOnCompositeClickAction: (compositeId: string) => IAction; + fillExtraContextMenuActions: (actions: IAction[]) => void; + getContextMenuActionsForComposite: (compositeId: string) => IAction[]; openComposite: (compositeId: string) => Promise; getDefaultCompositeId: () => string; hidePart: () => void; @@ -208,15 +208,15 @@ export class CompositeBar extends Widget implements ICompositeBar { create(parent: HTMLElement): HTMLElement { const actionBarDiv = parent.appendChild($('.composite-bar')); this.compositeSwitcherBar = this._register(new ActionBar(actionBarDiv, { - actionViewItemProvider: (action: IAction) => { + actionViewItemProvider: action => { if (action instanceof CompositeOverflowActivityAction) { return this.compositeOverflowActionViewItem; } const item = this.model.findItem(action.id); return item && this.instantiationService.createInstance( CompositeActionViewItem, action as ActivityAction, item.pinnedAction, - (compositeId: string) => this.options.getContextMenuActionsForComposite(compositeId), - () => this.getContextMenuActions() as Action[], + compositeId => this.options.getContextMenuActionsForComposite(compositeId), + () => this.getContextMenuActions(), this.options.colors, this.options.icon, this.options.dndHandler, @@ -319,7 +319,7 @@ export class CompositeBar extends Widget implements ICompositeBar { this.updateCompositeSwitcher(); } - addComposite({ id, name, order, requestedIndex }: { id: string; name: string, order?: number, requestedIndex?: number }): void { + addComposite({ id, name, order, requestedIndex }: { id: string; name: string, order?: number, requestedIndex?: number; }): void { // Add to the model if (this.model.add(id, name, order, requestedIndex)) { this.computeSizes([this.model.findItem(id)]); @@ -596,7 +596,7 @@ export class CompositeBar extends Widget implements ICompositeBar { this.compositeOverflowAction, () => this.getOverflowingComposites(), () => this.model.activeItem ? this.model.activeItem.id : undefined, - (compositeId: string) => { + compositeId => { const item = this.model.findItem(compositeId); return item?.activity[0]?.badge; }, @@ -610,7 +610,7 @@ export class CompositeBar extends Widget implements ICompositeBar { this._onDidChange.fire(); } - private getOverflowingComposites(): { id: string, name?: string }[] { + private getOverflowingComposites(): { id: string, name?: string; }[] { let overflowingIds = this.model.visibleItems.filter(item => item.pinned).map(item => item.id); // Show the active composite even if it is not pinned @@ -631,9 +631,9 @@ export class CompositeBar extends Widget implements ICompositeBar { }); } - private getContextMenuActions(): IAction[] { + getContextMenuActions(): IAction[] { const actions: IAction[] = this.model.visibleItems - .map(({ id, name, activityAction }) => ({ + .map(({ id, name, activityAction }) => (toAction({ id, label: this.getAction(id).label || name || id, checked: this.isPinned(id), @@ -645,19 +645,17 @@ export class CompositeBar extends Widget implements ICompositeBar { this.pin(id, true); } } - })); - const otherActions = this.options.getContextMenuActions(); - if (otherActions.length) { - actions.push(new Separator()); - actions.push(...otherActions); - } + }))); + + this.options.fillExtraContextMenuActions(actions); + return actions; } } interface ICompositeBarModelItem extends ICompositeBarItem { activityAction: ActivityAction; - pinnedAction: Action; + pinnedAction: IAction; activity: ICompositeActivity[]; } diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index be07132e5..779b309c5 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { Action, Separator } from 'vs/base/common/actions'; +import { Action, IAction, Separator } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { dispose, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; @@ -374,14 +374,14 @@ export class CompositeOverflowActivityAction extends ActivityAction { } export class CompositeOverflowActivityActionViewItem extends ActivityActionViewItem { - private actions: Action[] = []; + private actions: IAction[] = []; constructor( action: ActivityAction, private getOverflowingComposites: () => { id: string, name?: string }[], private getActiveCompositeId: () => string | undefined, private getBadge: (compositeId: string) => IBadge, - private getCompositeOpenAction: (compositeId: string) => Action, + private getCompositeOpenAction: (compositeId: string) => IAction, colors: (theme: IColorTheme) => ICompositeBarColors, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IThemeService themeService: IThemeService @@ -404,7 +404,7 @@ export class CompositeOverflowActivityActionViewItem extends ActivityActionViewI }); } - private getActions(): Action[] { + private getActions(): IAction[] { return this.getOverflowingComposites().map(composite => { const action = this.getCompositeOpenAction(composite.id); action.checked = this.getActiveCompositeId() === action.id; @@ -457,9 +457,9 @@ export class CompositeActionViewItem extends ActivityActionViewItem { constructor( private compositeActivityAction: ActivityAction, - private toggleCompositePinnedAction: Action, - private compositeContextMenuActionsProvider: (compositeId: string) => ReadonlyArray, - private contextMenuActionsProvider: () => ReadonlyArray, + private toggleCompositePinnedAction: IAction, + private compositeContextMenuActionsProvider: (compositeId: string) => IAction[], + private contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, icon: boolean, private dndHandler: ICompositeDragAndDrop, @@ -500,9 +500,14 @@ export class CompositeActionViewItem extends ActivityActionViewItem { return this.compositeActivity; } - private getActivtyName(): string { + private getActivtyName(skipKeybinding = false): string { + let name = this.compositeActivityAction.activity.name; + if (skipKeybinding) { + return name; + } + const keybinding = this.compositeActivityAction.activity.keybindingId ? this.keybindingService.lookupKeybinding(this.compositeActivityAction.activity.keybindingId) : null; - return keybinding ? nls.localize('titleKeybinding', "{0} ({1})", this.compositeActivityAction.activity.name, keybinding.getLabel()) : this.compositeActivityAction.activity.name; + return keybinding ? nls.localize('titleKeybinding', "{0} ({1})", name, keybinding.getLabel()) : name; } render(container: HTMLElement): void { @@ -601,7 +606,7 @@ export class CompositeActionViewItem extends ActivityActionViewItem { } private showContextMenu(container: HTMLElement): void { - const actions: Action[] = [this.toggleCompositePinnedAction]; + const actions: IAction[] = [this.toggleCompositePinnedAction]; const compositeContextMenuActions = this.compositeContextMenuActionsProvider(this.activity.id); if (compositeContextMenuActions.length) { @@ -615,10 +620,10 @@ export class CompositeActionViewItem extends ActivityActionViewItem { const isPinned = this.compositeBar.isPinned(this.activity.id); if (isPinned) { - this.toggleCompositePinnedAction.label = nls.localize('hide', "Hide"); + this.toggleCompositePinnedAction.label = nls.localize('hide', "Hide '{0}'", this.getActivtyName(true)); this.toggleCompositePinnedAction.checked = false; } else { - this.toggleCompositePinnedAction.label = nls.localize('keep', "Keep"); + this.toggleCompositePinnedAction.label = nls.localize('keep', "Keep '{0}'", this.getActivtyName(true)); } const otherActions = this.contextMenuActionsProvider(); diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 1350ddfe5..13fe045e5 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -327,10 +327,6 @@ export abstract class CompositePart extends Part { const primaryActions: IAction[] = composite?.getActions().slice(0) || []; const secondaryActions: IAction[] = composite?.getSecondaryActions().slice(0) || []; - // From Part - primaryActions.push(...this.getActions()); - secondaryActions.push(...this.getSecondaryActions()); - // Update context const toolBar = assertIsDefined(this.toolBar); toolBar.context = this.actionsContextProvider(); @@ -471,14 +467,6 @@ export abstract class CompositePart extends Part { return compositeItem ? compositeItem.progress : undefined; } - protected getActions(): ReadonlyArray { - return []; - } - - protected getSecondaryActions(): ReadonlyArray { - return []; - } - protected getTitleAreaDropDownAnchorAlignment(): AnchorAlignment { return AnchorAlignment.RIGHT; } diff --git a/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts b/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts index b5fc1d1fa..836303dbf 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts @@ -19,9 +19,9 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { Disposable } from 'vs/base/common/lifecycle'; export class DialogHandlerContribution extends Disposable implements IWorkbenchContribution { - private impl: IDialogHandler; + private readonly model: IDialogsModel; + private readonly impl: IDialogHandler; - private model: IDialogsModel; private currentDialog: IDialogViewItem | undefined; constructor( diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 541ea9b2c..026581b29 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -6,19 +6,13 @@ import * as dom from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { BreadcrumbsItem, BreadcrumbsWidget, IBreadcrumbsItemEvent } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget'; -import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { tail } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { combinedDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { extUri } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/breadcrumbscontrol'; -import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { Range } from 'vs/editor/common/core/range'; -import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; -import { SymbolKinds } from 'vs/editor/common/modes'; -import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { localize } from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -28,35 +22,92 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IListService, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; +import { IListService, WorkbenchDataTree, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ColorIdentifier, ColorFunction } from 'vs/platform/theme/common/colorRegistry'; import { attachBreadcrumbsStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ResourceLabel } from 'vs/workbench/browser/labels'; import { BreadcrumbsConfig, IBreadcrumbsService } from 'vs/workbench/browser/parts/editor/breadcrumbs'; -import { BreadcrumbElement, EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; -import { BreadcrumbsPicker, createBreadcrumbsPicker } from 'vs/workbench/browser/parts/editor/breadcrumbsPicker'; +import { BreadcrumbsModel, FileElement, OutlineElement2 } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; +import { BreadcrumbsFilePicker, BreadcrumbsOutlinePicker, BreadcrumbsPicker } from 'vs/workbench/browser/parts/editor/breadcrumbsPicker'; import { IEditorPartOptions, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { ACTIVE_GROUP, ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { onDidChangeZoomLevel } from 'vs/base/browser/browser'; -import { withNullAsUndefined, withUndefinedAsNull } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { CATEGORIES } from 'vs/workbench/common/actions'; +import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { IOutline } from 'vs/workbench/services/outline/browser/outline'; -class Item extends BreadcrumbsItem { +class OutlineItem extends BreadcrumbsItem { private readonly _disposables = new DisposableStore(); constructor( - readonly element: BreadcrumbElement, + readonly model: BreadcrumbsModel, + readonly element: OutlineElement2, + readonly options: IBreadcrumbsControlOptions + ) { + super(); + } + + dispose(): void { + this._disposables.dispose(); + } + + equals(other: BreadcrumbsItem): boolean { + if (!(other instanceof OutlineItem)) { + return false; + } + return this.element === other.element && + this.options.showFileIcons === other.options.showFileIcons && + this.options.showSymbolIcons === other.options.showSymbolIcons; + } + + render(container: HTMLElement): void { + const { element, outline } = this.element; + + if (element === outline) { + const element = dom.$('span', undefined, '…'); + container.appendChild(element); + return; + } + + const templateId = outline.config.delegate.getTemplateId(element); + const renderer = outline.config.renderers.find(renderer => renderer.templateId === templateId); + if (!renderer) { + container.innerText = '<>'; + return; + } + + const template = renderer.renderTemplate(container); + renderer.renderElement(>{ + element, + children: [], + depth: 0, + visibleChildrenCount: 0, + visibleChildIndex: 0, + collapsible: false, + collapsed: false, + visible: true, + filterData: undefined + }, 0, template, undefined); + + this._disposables.add(toDisposable(() => { renderer.disposeTemplate(template); })); + } + +} + +class FileItem extends BreadcrumbsItem { + + private readonly _disposables = new DisposableStore(); + + constructor( + readonly model: BreadcrumbsModel, + readonly element: FileElement, readonly options: IBreadcrumbsControlOptions, @IInstantiationService private readonly _instantiationService: IInstantiationService ) { @@ -68,59 +119,26 @@ class Item extends BreadcrumbsItem { } equals(other: BreadcrumbsItem): boolean { - if (!(other instanceof Item)) { + if (!(other instanceof FileItem)) { return false; } - if (this.element instanceof FileElement && other.element instanceof FileElement) { - return (extUri.isEqual(this.element.uri, other.element.uri) && - this.options.showFileIcons === other.options.showFileIcons && - this.options.showSymbolIcons === other.options.showSymbolIcons); - } - if (this.element instanceof TreeElement && other.element instanceof TreeElement) { - return this.element.id === other.element.id; - } - return false; + return (extUri.isEqual(this.element.uri, other.element.uri) && + this.options.showFileIcons === other.options.showFileIcons && + this.options.showSymbolIcons === other.options.showSymbolIcons); + } render(container: HTMLElement): void { - if (this.element instanceof FileElement) { - // file/folder - let label = this._instantiationService.createInstance(ResourceLabel, container, {}); - label.element.setFile(this.element.uri, { - hidePath: true, - hideIcon: this.element.kind === FileKind.FOLDER || !this.options.showFileIcons, - fileKind: this.element.kind, - fileDecorations: { colors: this.options.showDecorationColors, badges: false }, - }); - container.classList.add(FileKind[this.element.kind].toLowerCase()); - this._disposables.add(label); - - } else if (this.element instanceof OutlineModel) { - // has outline element but not in one - let label = document.createElement('div'); - label.innerText = '\u2026'; - label.className = 'hint-more'; - container.appendChild(label); - - } else if (this.element instanceof OutlineGroup) { - // provider - let label = new IconLabel(container); - label.setLabel(this.element.label); - this._disposables.add(label); - - } else if (this.element instanceof OutlineElement) { - // symbol - if (this.options.showSymbolIcons) { - let icon = document.createElement('div'); - icon.className = SymbolKinds.toCssClassName(this.element.symbol.kind); - container.appendChild(icon); - container.classList.add('shows-symbol-icon'); - } - let label = new IconLabel(container); - let title = this.element.symbol.name.replace(/\r|\n|\r\n/g, '\u23CE'); - label.setLabel(title); - this._disposables.add(label); - } + // file/folder + let label = this._instantiationService.createInstance(ResourceLabel, container, {}); + label.element.setFile(this.element.uri, { + hidePath: true, + hideIcon: this.element.kind === FileKind.FOLDER || !this.options.showFileIcons, + fileKind: this.element.kind, + fileDecorations: { colors: this.options.showDecorationColors, badges: false }, + }); + container.classList.add(FileKind[this.element.kind].toLowerCase()); + this._disposables.add(label); } } @@ -170,26 +188,23 @@ export class BreadcrumbsControl { private readonly _editorGroup: IEditorGroupView, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IContextViewService private readonly _contextViewService: IContextViewService, - @IEditorService private readonly _editorService: IEditorService, - @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IThemeService private readonly _themeService: IThemeService, @IQuickInputService private readonly _quickInputService: IQuickInputService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, @IFileService private readonly _fileService: IFileService, @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IEditorService private readonly _editorService: IEditorService, @ILabelService private readonly _labelService: ILabelService, + @IConfigurationService configurationService: IConfigurationService, @IBreadcrumbsService breadcrumbsService: IBreadcrumbsService, ) { this.domNode = document.createElement('div'); this.domNode.classList.add('breadcrumbs-control'); dom.append(container, this.domNode); - this._cfUseQuickPick = BreadcrumbsConfig.UseQuickPick.bindTo(_configurationService); - this._cfShowIcons = BreadcrumbsConfig.Icons.bindTo(_configurationService); - this._cfTitleScrollbarSizing = BreadcrumbsConfig.TitleScrollbarSizing.bindTo(_configurationService); + this._cfUseQuickPick = BreadcrumbsConfig.UseQuickPick.bindTo(configurationService); + this._cfShowIcons = BreadcrumbsConfig.Icons.bindTo(configurationService); + this._cfTitleScrollbarSizing = BreadcrumbsConfig.TitleScrollbarSizing.bindTo(configurationService); const sizing = this._cfTitleScrollbarSizing.getValue() ?? 'default'; this._widget = new BreadcrumbsWidget(this.domNode, BreadcrumbsControl.SCROLLBAR_SIZES[sizing]); @@ -256,14 +271,11 @@ export class BreadcrumbsControl { this._ckBreadcrumbsVisible.set(true); this._ckBreadcrumbsPossible.set(true); - const editor = this._getActiveCodeEditor(); - const model = new EditorBreadcrumbsModel( + const model = this._instantiationService.createInstance(BreadcrumbsModel, fileInfoUri ?? uri, - uri, editor, - this._configurationService, - this._textResourceConfigurationService, - this._workspaceService + this._editorGroup.activeEditorPane ); + this.domNode.classList.toggle('relative-path', model.isRelative()); this.domNode.classList.toggle('backslash-path', this._labelService.getSeparator(uri.scheme, uri.authority) === '\\'); @@ -274,7 +286,7 @@ export class BreadcrumbsControl { showFileIcons: this._options.showFileIcons && showIcons, showSymbolIcons: this._options.showSymbolIcons && showIcons }; - const items = model.getElements().map(element => new Item(element, options, this._instantiationService)); + const items = model.getElements().map(element => element instanceof FileElement ? new FileItem(model, element, options, this._instantiationService) : new OutlineItem(model, element, options)); this._widget.setItems(items); this._widget.reveal(items[items.length - 1]); }; @@ -298,7 +310,7 @@ export class BreadcrumbsControl { this._breadcrumbsDisposables.add({ dispose: () => { if (this._breadcrumbsPickerShowing) { - this._contextViewService.hideContextView(this); + this._contextViewService.hideContextView({ source: this }); } } }); @@ -306,20 +318,6 @@ export class BreadcrumbsControl { return true; } - private _getActiveCodeEditor(): ICodeEditor | undefined { - if (!this._editorGroup.activeEditorPane) { - return undefined; - } - let control = this._editorGroup.activeEditorPane.getControl(); - let editor: ICodeEditor | undefined; - if (isCodeEditor(control)) { - editor = control as ICodeEditor; - } else if (isDiffEditor(control)) { - editor = control.getModifiedEditor(); - } - return editor; - } - private _onFocusEvent(event: IBreadcrumbsItemEvent): void { if (event.item && this._breadcrumbsPickerShowing) { this._breadcrumbsPickerIgnoreOnceItem = undefined; @@ -339,13 +337,12 @@ export class BreadcrumbsControl { return; } - const { element } = event.item as Item; + const { element } = event.item as FileItem | OutlineItem; this._editorGroup.focus(); - type BreadcrumbSelectClassification = { - type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - }; - this._telemetryService.publicLog2<{ type: string }, BreadcrumbSelectClassification>('breadcrumbs/select', { type: element instanceof TreeElement ? 'symbol' : 'file' }); + type BreadcrumbSelect = { type: string }; + type BreadcrumbSelectClassification = { type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; + this._telemetryService.publicLog2('breadcrumbs/select', { type: event.item instanceof OutlineItem ? 'symbol' : 'file' }); const group = this._getEditorGroup(event.payload); if (group !== undefined) { @@ -360,64 +357,31 @@ export class BreadcrumbsControl { // using quick pick this._widget.setFocused(undefined); this._widget.setSelection(undefined); - this._quickInputService.quickAccess.show(element instanceof TreeElement ? '@' : ''); + this._quickInputService.quickAccess.show(element instanceof OutlineElement2 ? '@' : ''); return; } // show picker let picker: BreadcrumbsPicker; let pickerAnchor: { x: number; y: number }; - let editor = this._getActiveCodeEditor(); - let editorDecorations: string[] = []; - let editorViewState: ICodeEditorViewState | undefined; + + interface IHideData { didPick?: boolean, source?: BreadcrumbsControl } this._contextViewService.showContextView({ render: (parent: HTMLElement) => { - picker = createBreadcrumbsPicker(this._instantiationService, parent, element); - let selectListener = picker.onDidPickElement(data => { - if (data.target) { - editorViewState = undefined; - } - this._contextViewService.hideContextView(this); + if (event.item instanceof FileItem) { + picker = this._instantiationService.createInstance(BreadcrumbsFilePicker, parent, event.item.model.resource); + } else if (event.item instanceof OutlineItem) { + picker = this._instantiationService.createInstance(BreadcrumbsOutlinePicker, parent, event.item.model.resource); + } - const group = (picker.useAltAsMultipleSelectionModifier && (data.browserEvent as MouseEvent).metaKey) || (!picker.useAltAsMultipleSelectionModifier && (data.browserEvent as MouseEvent).altKey) - ? SIDE_GROUP - : ACTIVE_GROUP; - - this._revealInEditor(event, data.target, group, (data.browserEvent as MouseEvent).button === 1); - /* __GDPR__ - "breadcrumbs/open" : { - "type": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this._telemetryService.publicLog('breadcrumbs/open', { type: !data ? 'nothing' : data.target instanceof TreeElement ? 'symbol' : 'file' }); - }); - let focusListener = picker.onDidFocusElement(data => { - if (!editor || !(data.target instanceof OutlineElement)) { - return; - } - if (!editorViewState) { - editorViewState = withNullAsUndefined(editor.saveViewState()); - } - const { symbol } = data.target; - editor.revealRangeInCenterIfOutsideViewport(symbol.range, ScrollType.Smooth); - editorDecorations = editor.deltaDecorations(editorDecorations, [{ - range: symbol.range, - options: { - className: 'rangeHighlight', - isWholeLine: true - } - }]); - }); - - let zoomListener = onDidChangeZoomLevel(() => { - this._contextViewService.hideContextView(this); - }); + let selectListener = picker.onWillPickElement(() => this._contextViewService.hideContextView({ source: this, didPick: true })); + let zoomListener = onDidChangeZoomLevel(() => this._contextViewService.hideContextView({ source: this })); let focusTracker = dom.trackFocus(parent); let blurListener = focusTracker.onDidBlur(() => { this._breadcrumbsPickerIgnoreOnceItem = this._widget.isDOMFocused() ? event.item : undefined; - this._contextViewService.hideContextView(this); + this._contextViewService.hideContextView({ source: this }); }); this._breadcrumbsPickerShowing = true; @@ -426,7 +390,6 @@ export class BreadcrumbsControl { return combinedDisposable( picker, selectListener, - focusListener, zoomListener, focusTracker, blurListener @@ -465,19 +428,17 @@ export class BreadcrumbsControl { } return pickerAnchor; }, - onHide: (data) => { - if (editor) { - editor.deltaDecorations(editorDecorations, []); - if (editorViewState) { - editor.restoreViewState(editorViewState); - } + onHide: (data?: IHideData) => { + if (!data?.didPick) { + picker.restoreViewState(); } this._breadcrumbsPickerShowing = false; this._updateCkBreadcrumbsActive(); - if (data === this) { + if (data?.source === this) { this._widget.setFocused(undefined); this._widget.setSelection(undefined); } + picker.dispose(); } }); } @@ -487,11 +448,11 @@ export class BreadcrumbsControl { this._ckBreadcrumbsActive.set(value); } - private _revealInEditor(event: IBreadcrumbsItemEvent, element: BreadcrumbElement, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined, pinned: boolean = false): void { + private async _revealInEditor(event: IBreadcrumbsItemEvent, element: FileElement | OutlineElement2, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined, pinned: boolean = false): Promise { + if (element instanceof FileElement) { if (element.kind === FileKind.FILE) { - // open file in any editor - this._editorService.openEditor({ resource: element.uri, options: { pinned } }, group); + await this._editorService.openEditor({ resource: element.uri, options: { pinned } }, group); } else { // show next picker let items = this._widget.getItems(); @@ -499,20 +460,8 @@ export class BreadcrumbsControl { this._widget.setFocused(items[idx + 1]); this._widget.setSelection(items[idx + 1], BreadcrumbsControl.Payload_Pick); } - - } else if (element instanceof OutlineElement) { - // open symbol in code editor - const model = OutlineModel.get(element); - if (model) { - this._codeEditorService.openCodeEditor({ - resource: model.uri, - options: { - selection: Range.collapseToStart(element.symbol.selectionRange), - selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport, - pinned - } - }, withUndefinedAsNull(this._getActiveCodeEditor()), group === SIDE_GROUP); - } + } else { + element.outline.reveal(element, { pinned }, group === SIDE_GROUP); } } @@ -744,29 +693,29 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler(accessor) { const editors = accessor.get(IEditorService); const lists = accessor.get(IListService); - const element = lists.lastFocusedList ? lists.lastFocusedList.getFocus()[0] : undefined; - if (element instanceof OutlineElement) { - const outlineElement = OutlineModel.get(element); - if (!outlineElement) { - return undefined; - } - // open symbol in editor - return editors.openEditor({ - resource: outlineElement.uri, - options: { selection: Range.collapseToStart(element.symbol.selectionRange), pinned: true } - }, SIDE_GROUP); + const tree = lists.lastFocusedList; + if (!(tree instanceof WorkbenchDataTree)) { + return; + } - } else if (element && URI.isUri(element.resource)) { - // open file in editor + const element = tree.getFocus()[0]; + + if (URI.isUri((element)?.resource)) { + // IFileStat: open file in editor return editors.openEditor({ - resource: element.resource, + resource: (element).resource, options: { pinned: true } }, SIDE_GROUP); + } - } else { - // ignore - return undefined; + // IOutline: check if this the outline and iff so reveal element + const input = tree.getInput(); + if (input && typeof (>input).outlineKind === 'string') { + return (>input).reveal(element, { + pinned: true, + preserveFocus: false + }, true); } } }); diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts index 01ff3916e..4364969d2 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -3,27 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equals } from 'vs/base/common/arrays'; -import { TimeoutTimer } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { isEqual, dirname } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IPosition } from 'vs/editor/common/core/position'; -import { DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; -import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { Schemas } from 'vs/base/common/network'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; import { FileKind } from 'vs/platform/files/common/files'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { OutlineFilter } from 'vs/editor/contrib/documentSymbols/outlineTree'; -import { ITextModel } from 'vs/editor/common/model'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IOutline, IOutlineService, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; +import { IEditorPane } from 'vs/workbench/common/editor'; export class FileElement { constructor( @@ -32,11 +25,16 @@ export class FileElement { ) { } } -export type BreadcrumbElement = FileElement | OutlineModel | OutlineGroup | OutlineElement; - type FileInfo = { path: FileElement[], folder?: IWorkspaceFolder }; -export class EditorBreadcrumbsModel { +export class OutlineElement2 { + constructor( + readonly element: IOutline | any, + readonly outline: IOutline + ) { } +} + +export class BreadcrumbsModel { private readonly _disposables = new DisposableStore(); private readonly _fileInfo: FileInfo; @@ -45,28 +43,31 @@ export class EditorBreadcrumbsModel { private readonly _cfgFilePath: BreadcrumbsConfig<'on' | 'off' | 'last'>; private readonly _cfgSymbolPath: BreadcrumbsConfig<'on' | 'off' | 'last'>; - private _outlineElements: Array = []; - private _outlineDisposables = new DisposableStore(); + private readonly _currentOutline = new MutableDisposable>(); + private readonly _outlineDisposables = new DisposableStore(); private readonly _onDidUpdate = new Emitter(); readonly onDidUpdate: Event = this._onDidUpdate.event; constructor( - fileInfoUri: URI, - private readonly _uri: URI, - private readonly _editor: ICodeEditor | undefined, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, - @IWorkspaceContextService workspaceService: IWorkspaceContextService, + readonly resource: URI, + editor: IEditorPane | undefined, + @IConfigurationService configurationService: IConfigurationService, + @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + @IOutlineService private readonly _outlineService: IOutlineService, ) { - this._cfgEnabled = BreadcrumbsConfig.IsEnabled.bindTo(_configurationService); - this._cfgFilePath = BreadcrumbsConfig.FilePath.bindTo(_configurationService); - this._cfgSymbolPath = BreadcrumbsConfig.SymbolPath.bindTo(_configurationService); + this._cfgEnabled = BreadcrumbsConfig.IsEnabled.bindTo(configurationService); + this._cfgFilePath = BreadcrumbsConfig.FilePath.bindTo(configurationService); + this._cfgSymbolPath = BreadcrumbsConfig.SymbolPath.bindTo(configurationService); this._disposables.add(this._cfgFilePath.onDidChange(_ => this._onDidUpdate.fire(this))); this._disposables.add(this._cfgSymbolPath.onDidChange(_ => this._onDidUpdate.fire(this))); - this._fileInfo = EditorBreadcrumbsModel._initFilePathInfo(fileInfoUri, workspaceService); - this._bindToEditor(); + this._fileInfo = this._initFilePathInfo(resource); + + if (editor) { + this._bindToEditor(editor); + this._disposables.add(_outlineService.onDidChange(() => this._bindToEditor(editor))); + } this._onDidUpdate.fire(this); } @@ -74,6 +75,7 @@ export class EditorBreadcrumbsModel { this._cfgEnabled.dispose(); this._cfgFilePath.dispose(); this._cfgSymbolPath.dispose(); + this._currentOutline.dispose(); this._outlineDisposables.dispose(); this._disposables.dispose(); this._onDidUpdate.dispose(); @@ -83,8 +85,8 @@ export class EditorBreadcrumbsModel { return Boolean(this._fileInfo.folder); } - getElements(): ReadonlyArray { - let result: BreadcrumbElement[] = []; + getElements(): ReadonlyArray { + let result: (FileElement | OutlineElement2)[] = []; // file path elements if (this._cfgFilePath.getValue() === 'on') { @@ -93,17 +95,27 @@ export class EditorBreadcrumbsModel { result = result.concat(this._fileInfo.path.slice(-1)); } - // symbol path elements - if (this._cfgSymbolPath.getValue() === 'on') { - result = result.concat(this._outlineElements); - } else if (this._cfgSymbolPath.getValue() === 'last' && this._outlineElements.length > 0) { - result = result.concat(this._outlineElements.slice(-1)); + if (this._cfgSymbolPath.getValue() === 'off') { + return result; + } + + if (!this._currentOutline.value) { + return result; + } + + const breadcrumbsElements = this._currentOutline.value.config.breadcrumbsDataSource.getBreadcrumbElements(); + for (let i = this._cfgSymbolPath.getValue() === 'last' && breadcrumbsElements.length > 0 ? breadcrumbsElements.length - 1 : 0; i < breadcrumbsElements.length; i++) { + result.push(new OutlineElement2(breadcrumbsElements[i], this._currentOutline.value)); + } + + if (breadcrumbsElements.length === 0 && !this._currentOutline.value.isEmpty) { + result.push(new OutlineElement2(this._currentOutline.value, this._currentOutline.value)); } return result; } - private static _initFilePathInfo(uri: URI, workspaceService: IWorkspaceContextService): FileInfo { + private _initFilePathInfo(uri: URI): FileInfo { if (uri.scheme === Schemas.untitled) { return { @@ -113,7 +125,7 @@ export class EditorBreadcrumbsModel { } let info: FileInfo = { - folder: withNullAsUndefined(workspaceService.getWorkspaceFolder(uri)), + folder: withNullAsUndefined(this._workspaceService.getWorkspaceFolder(uri)), path: [] }; @@ -130,181 +142,33 @@ export class EditorBreadcrumbsModel { } } - if (info.folder && workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE) { + if (info.folder && this._workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE) { info.path.unshift(new FileElement(info.folder.uri, FileKind.ROOT_FOLDER)); } return info; } - private _bindToEditor(): void { - if (!this._editor) { - return; - } - // update as language, model, providers changes - this._disposables.add(DocumentSymbolProviderRegistry.onDidChange(_ => this._updateOutline())); - this._disposables.add(this._editor.onDidChangeModel(_ => this._updateOutline())); - this._disposables.add(this._editor.onDidChangeModelLanguage(_ => this._updateOutline())); - - // update when config changes (re-render) - this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { - if (!this._cfgEnabled.getValue()) { - // breadcrumbs might be disabled (also via a setting/config) and that is - // something we must check before proceeding. - return; - } - if (e.affectsConfiguration('breadcrumbs')) { - this._updateOutline(true); - return; - } - if (this._editor && this._editor.getModel()) { - const editorModel = this._editor.getModel() as ITextModel; - const languageName = editorModel.getLanguageIdentifier().language; - - // Checking for changes in the current language override config. - // We can't be more specific than this because the ConfigurationChangeEvent(e) only includes the first part of the root path - if (e.affectsConfiguration(`[${languageName}]`)) { - this._updateOutline(true); - } - } - })); - - - // update soon'ish as model content change - const updateSoon = new TimeoutTimer(); - this._disposables.add(updateSoon); - this._disposables.add(this._editor.onDidChangeModelContent(_ => { - const timeout = OutlineModel.getRequestDelay(this._editor!.getModel()); - updateSoon.cancelAndSet(() => this._updateOutline(true), timeout); - })); - this._updateOutline(); - - // stop when editor dies - this._disposables.add(this._editor.onDidDispose(() => this._outlineDisposables.clear())); - } - - private _updateOutline(didChangeContent?: boolean): void { - + private _bindToEditor(editor: IEditorPane): void { + const newCts = new CancellationTokenSource(); + this._currentOutline.clear(); this._outlineDisposables.clear(); - if (!didChangeContent) { - this._updateOutlineElements([]); - } + this._outlineDisposables.add(toDisposable(() => newCts.dispose(true))); - const editor = this._editor!; - - const buffer = editor.getModel(); - if (!buffer || !DocumentSymbolProviderRegistry.has(buffer) || !isEqual(buffer.uri, this._uri)) { - return; - } - - const source = new CancellationTokenSource(); - const versionIdThen = buffer.getVersionId(); - const timeout = new TimeoutTimer(); - - this._outlineDisposables.add({ - dispose: () => { - source.dispose(true); - timeout.dispose(); + this._outlineService.createOutline(editor, OutlineTarget.Breadcrumbs, newCts.token).then(outline => { + if (newCts.token.isCancellationRequested) { + // cancelled: dispose new outline and reset + outline?.dispose(); + outline = undefined; } - }); - - OutlineModel.create(buffer, source.token).then(model => { - if (source.token.isCancellationRequested) { - // cancelled -> do nothing - return; + this._currentOutline.value = outline; + this._onDidUpdate.fire(this); + if (outline) { + this._outlineDisposables.add(outline.onDidChange(() => this._onDidUpdate.fire(this))); } - if (TreeElement.empty(model)) { - // empty -> no outline elements - this._updateOutlineElements([]); - } else { - // copy the model - model = model.adopt(); - - this._updateOutlineElements(this._getOutlineElements(model, editor.getPosition())); - this._outlineDisposables.add(editor.onDidChangeCursorPosition(_ => { - timeout.cancelAndSet(() => { - if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId() && editor.getModel()) { - this._updateOutlineElements(this._getOutlineElements(model, editor.getPosition())); - } - }, 150); - })); - } }).catch(err => { - this._updateOutlineElements([]); + this._onDidUpdate.fire(this); onUnexpectedError(err); }); } - - private _getOutlineElements(model: OutlineModel, position: IPosition | null): Array { - if (!model || !position) { - return []; - } - let item: OutlineGroup | OutlineElement | undefined = model.getItemEnclosingPosition(position); - if (!item) { - return this._getOutlineElementsRoot(model); - } - let chain: Array = []; - while (item) { - chain.push(item); - let parent: any = item.parent; - if (parent instanceof OutlineModel) { - break; - } - if (parent instanceof OutlineGroup && parent.parent && parent.parent.children.size === 1) { - break; - } - item = parent; - } - let result: Array = []; - for (let i = chain.length - 1; i >= 0; i--) { - let element = chain[i]; - if (this._isFiltered(element)) { - break; - } - result.push(element); - } - if (result.length === 0) { - return this._getOutlineElementsRoot(model); - } - return result; - } - - private _getOutlineElementsRoot(model: OutlineModel): (OutlineModel | OutlineGroup | OutlineElement)[] { - for (const child of model.children.values()) { - if (!this._isFiltered(child)) { - return [model]; - } - } - return []; - } - - private _isFiltered(element: TreeElement): boolean { - if (element instanceof OutlineElement) { - const key = `breadcrumbs.${OutlineFilter.kindToConfigName[element.symbol.kind]}`; - let uri: URI | undefined; - if (this._editor && this._editor.getModel()) { - const model = this._editor.getModel() as ITextModel; - uri = model.uri; - } - return !this._textResourceConfigurationService.getValue(uri, key); - } - return false; - } - - private _updateOutlineElements(elements: Array): void { - if (!equals(elements, this._outlineElements, EditorBreadcrumbsModel._outlineElementEquals)) { - this._outlineElements = elements; - this._onDidUpdate.fire(this); - } - } - - private static _outlineElementEquals(a: OutlineModel | OutlineGroup | OutlineElement, b: OutlineModel | OutlineGroup | OutlineElement): boolean { - if (a === b) { - return true; - } else if (!a || !b) { - return false; - } else { - return a.id === b.id; - } - } } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts index 001398ec0..223380e6c 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts @@ -8,34 +8,30 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; import * as glob from 'vs/base/common/glob'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, MutableDisposable, Disposable } from 'vs/base/common/lifecycle'; import { posix } from 'vs/base/common/path'; import { basename, dirname, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/breadcrumbscontrol'; -import { OutlineElement, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; -import { IConfigurationService, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WorkbenchDataTree, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { breadcrumbsPickerBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; -import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { isWorkspace, isWorkspaceFolder, IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ResourceLabels, IResourceLabel, DEFAULT_LABELS_CONTAINER } from 'vs/workbench/browser/labels'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; -import { BreadcrumbElement, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; +import { OutlineElement2, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; import { IAsyncDataSource, ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; -import { OutlineVirtualDelegate, OutlineGroupRenderer, OutlineElementRenderer, OutlineItemComparator, OutlineIdentityProvider, OutlineNavigationLabelProvider, OutlineDataSource, OutlineSortOrder, OutlineFilter, OutlineAccessibilityProvider } from 'vs/editor/contrib/documentSymbols/outlineTree'; import { IIdentityProvider, IListVirtualDelegate, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list'; import { IFileIconTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { IModeService } from 'vs/editor/common/services/modeService'; import { localize } from 'vs/nls'; - -export function createBreadcrumbsPicker(instantiationService: IInstantiationService, parent: HTMLElement, element: BreadcrumbElement): BreadcrumbsPicker { - return element instanceof FileElement - ? instantiationService.createInstance(BreadcrumbsFilePicker, parent) - : instantiationService.createInstance(BreadcrumbsOutlinePicker, parent); -} +import { IOutline, IOutlineComparator } from 'vs/workbench/services/outline/browser/outline'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; interface ILayoutInfo { maxHeight: number; @@ -62,17 +58,18 @@ export abstract class BreadcrumbsPicker { protected _fakeEvent = new UIEvent('fakeEvent'); protected _layoutInfo!: ILayoutInfo; - private readonly _onDidPickElement = new Emitter(); - readonly onDidPickElement: Event = this._onDidPickElement.event; + protected readonly _onWillPickElement = new Emitter(); + readonly onWillPickElement: Event = this._onWillPickElement.event; - private readonly _onDidFocusElement = new Emitter(); - readonly onDidFocusElement: Event = this._onDidFocusElement.event; + private readonly _previewDispoables = new MutableDisposable(); constructor( parent: HTMLElement, + protected resource: URI, @IInstantiationService protected readonly _instantiationService: IInstantiationService, @IThemeService protected readonly _themeService: IThemeService, @IConfigurationService protected readonly _configurationService: IConfigurationService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { this._domNode = document.createElement('div'); this._domNode.className = 'monaco-breadcrumbs-picker show-file-icons'; @@ -81,12 +78,13 @@ export abstract class BreadcrumbsPicker { dispose(): void { this._disposables.dispose(); - this._onDidPickElement.dispose(); - this._onDidFocusElement.dispose(); - this._tree.dispose(); + this._previewDispoables.dispose(); + this._onWillPickElement.dispose(); + this._domNode.remove(); + setTimeout(() => this._tree.dispose(), 0); // tree cannot be disposed while being opened... } - show(input: any, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): void { + async show(input: any, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): Promise { const theme = this._themeService.getColorTheme(); const color = theme.getColor(breadcrumbsPickerBackground); @@ -103,33 +101,33 @@ export abstract class BreadcrumbsPicker { this._domNode.appendChild(this._treeContainer); this._layoutInfo = { maxHeight, width, arrowSize, arrowOffset, inputHeight: 0 }; - this._tree = this._createTree(this._treeContainer); + this._tree = this._createTree(this._treeContainer, input); - this._disposables.add(this._tree.onDidChangeSelection(e => { - if (e.browserEvent !== this._fakeEvent) { - const target = this._getTargetFromEvent(e.elements[0]); - if (target) { - setTimeout(_ => {// need to debounce here because this disposes the tree and the tree doesn't like to be disposed on click - this._onDidPickElement.fire({ target, browserEvent: e.browserEvent || new UIEvent('fake') }); - }, 0); - } + this._disposables.add(this._tree.onDidOpen(async e => { + const { element, editorOptions, sideBySide } = e; + const didReveal = await this._revealElement(element, { ...editorOptions, preserveFocus: false }, sideBySide); + if (!didReveal) { + return; } + // send telemetry + interface OpenEvent { type: string } + interface OpenEventGDPR { type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' } } + this._telemetryService.publicLog2('breadcrumbs/open', { type: element instanceof OutlineElement2 ? 'symbol' : 'file' }); })); this._disposables.add(this._tree.onDidChangeFocus(e => { - const target = this._getTargetFromEvent(e.elements[0]); - if (target) { - this._onDidFocusElement.fire({ target, browserEvent: e.browserEvent || new UIEvent('fake') }); - } + this._previewDispoables.value = this._previewElement(e.elements[0]); })); this._disposables.add(this._tree.onDidChangeContentHeight(() => { this._layout(); })); this._domNode.focus(); - - this._setInput(input).then(() => { + try { + await this._setInput(input); this._layout(); - }).catch(onUnexpectedError); + } catch (err) { + onUnexpectedError(err); + } } protected _layout(): void { @@ -146,16 +144,15 @@ export abstract class BreadcrumbsPicker { this._treeContainer.style.height = `${treeHeight}px`; this._treeContainer.style.width = `${this._layoutInfo.width}px`; this._tree.layout(treeHeight, this._layoutInfo.width); - } - get useAltAsMultipleSelectionModifier() { - return this._tree.useAltAsMultipleSelectionModifier; - } + restoreViewState(): void { } + + protected abstract _setInput(element: FileElement | OutlineElement2): Promise; + protected abstract _createTree(container: HTMLElement, input: any): Tree; + protected abstract _previewElement(element: any): IDisposable; + protected abstract _revealElement(element: any, options: IEditorOptions, sideBySide: boolean): Promise; - protected abstract _setInput(element: BreadcrumbElement): Promise; - protected abstract _createTree(container: HTMLElement): Tree; - protected abstract _getTargetFromEvent(element: any): any | undefined; } //#region - Files @@ -173,9 +170,9 @@ class FileIdentityProvider implements IIdentityProvider { - private readonly _parents = new WeakMap(); - constructor( @IFileService private readonly _fileService: IFileService, ) { } hasChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): boolean { return URI.isUri(element) - || IWorkspace.isIWorkspace(element) - || IWorkspaceFolder.isIWorkspaceFolder(element) + || isWorkspace(element) + || isWorkspaceFolder(element) || element.isDirectory; } - getChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): Promise<(IWorkspaceFolder | IFileStat)[]> { - - if (IWorkspace.isIWorkspace(element)) { - return Promise.resolve(element.folders).then(folders => { - for (let child of folders) { - this._parents.set(element, child); - } - return folders; - }); + async getChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): Promise<(IWorkspaceFolder | IFileStat)[]> { + if (isWorkspace(element)) { + return element.folders; } let uri: URI; - if (IWorkspaceFolder.isIWorkspaceFolder(element)) { + if (isWorkspaceFolder(element)) { uri = element.uri; } else if (URI.isUri(element)) { uri = element; } else { uri = element.resource; } - return this._fileService.resolve(uri).then(stat => { - for (const child of stat.children || []) { - this._parents.set(stat, child); - } - return stat.children || []; - }); + const stat = await this._fileService.resolve(uri); + return stat.children ?? []; } } @@ -245,7 +230,7 @@ class FileRenderer implements ITreeRenderer { } filter(element: IWorkspaceFolder | IFileStat, _parentVisibility: TreeVisibility): boolean { - if (IWorkspaceFolder.isIWorkspaceFolder(element)) { + if (isWorkspaceFolder(element)) { // not a file return true; } @@ -345,7 +330,7 @@ class FileFilter implements ITreeFilter { export class FileSorter implements ITreeSorter { compare(a: IFileStat | IWorkspaceFolder, b: IFileStat | IWorkspaceFolder): number { - if (IWorkspaceFolder.isIWorkspaceFolder(a) && IWorkspaceFolder.isIWorkspaceFolder(b)) { + if (isWorkspaceFolder(a) && isWorkspaceFolder(b)) { return a.index - b.index; } if ((a as IFileStat).isDirectory === (b as IFileStat).isDirectory) { @@ -363,12 +348,15 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { constructor( parent: HTMLElement, + protected resource: URI, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IConfigurationService configService: IConfigurationService, @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + @IEditorService private readonly _editorService: IEditorService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(parent, instantiationService, themeService, configService); + super(parent, resource, instantiationService, themeService, configService, telemetryService); } _createTree(container: HTMLElement) { @@ -406,7 +394,7 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { }); } - _setInput(element: BreadcrumbElement): Promise { + async _setInput(element: FileElement | OutlineElement2): Promise { const { uri, kind } = (element as FileElement); let input: IWorkspace | URI; if (kind === FileKind.ROOT_FOLDER) { @@ -416,115 +404,120 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { } const tree = this._tree as WorkbenchAsyncDataTree; - return tree.setInput(input).then(() => { - let focusElement: IWorkspaceFolder | IFileStat | undefined; - for (const { element } of tree.getNode().children) { - if (IWorkspaceFolder.isIWorkspaceFolder(element) && isEqual(element.uri, uri)) { - focusElement = element; - break; - } else if (isEqual((element as IFileStat).resource, uri)) { - focusElement = element as IFileStat; - break; - } + await tree.setInput(input); + let focusElement: IWorkspaceFolder | IFileStat | undefined; + for (const { element } of tree.getNode().children) { + if (isWorkspaceFolder(element) && isEqual(element.uri, uri)) { + focusElement = element; + break; + } else if (isEqual((element as IFileStat).resource, uri)) { + focusElement = element as IFileStat; + break; } - if (focusElement) { - tree.reveal(focusElement, 0.5); - tree.setFocus([focusElement], this._fakeEvent); - } - tree.domFocus(); - }); + } + if (focusElement) { + tree.reveal(focusElement, 0.5); + tree.setFocus([focusElement], this._fakeEvent); + } + tree.domFocus(); } - protected _getTargetFromEvent(element: any): any | undefined { - if (element && !IWorkspaceFolder.isIWorkspaceFolder(element) && !(element as IFileStat).isDirectory) { - return new FileElement((element as IFileStat).resource, FileKind.FILE); + protected _previewElement(_element: any): IDisposable { + return Disposable.None; + } + + async _revealElement(element: IFileStat | IWorkspaceFolder, options: IEditorOptions, sideBySide: boolean): Promise { + let resource: URI | undefined; + if (isWorkspaceFolder(element)) { + resource = element.uri; + } else if (!element.isDirectory) { + resource = element.resource; } + if (resource) { + this._onWillPickElement.fire(); + await this._editorService.openEditor({ resource, options }, sideBySide ? SIDE_GROUP : undefined); + return true; + } + return false; } } //#endregion -//#region - Symbols +//#region - Outline + +class OutlineTreeSorter implements ITreeSorter { + + private _order: 'name' | 'type' | 'position'; + + constructor( + private comparator: IOutlineComparator, + uri: URI | undefined, + @ITextResourceConfigurationService configService: ITextResourceConfigurationService, + ) { + this._order = configService.getValue(uri, 'breadcrumbs.symbolSortOrder'); + } + + compare(a: E, b: E): number { + if (this._order === 'name') { + return this.comparator.compareByName(a, b); + } else if (this._order === 'type') { + return this.comparator.compareByType(a, b); + } else { + return this.comparator.compareByPosition(a, b); + } + } +} export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker { - protected readonly _symbolSortOrder: BreadcrumbsConfig<'position' | 'name' | 'type'>; - protected _outlineComparator: OutlineItemComparator; + protected _createTree(container: HTMLElement, input: OutlineElement2) { - constructor( - parent: HTMLElement, - @IInstantiationService instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, - @IConfigurationService configurationService: IConfigurationService, - @IModeService private readonly _modeService: IModeService, - ) { - super(parent, instantiationService, themeService, configurationService); - this._symbolSortOrder = BreadcrumbsConfig.SymbolSortOrder.bindTo(this._configurationService); - this._outlineComparator = new OutlineItemComparator(); - } + const { config } = input.outline; - protected _createTree(container: HTMLElement) { - return >this._instantiationService.createInstance( + return , any, FuzzyScore>>this._instantiationService.createInstance( WorkbenchDataTree, 'BreadcrumbsOutlinePicker', container, - new OutlineVirtualDelegate(), - [new OutlineGroupRenderer(), this._instantiationService.createInstance(OutlineElementRenderer)], - new OutlineDataSource(), + config.delegate, + config.renderers, + config.treeDataSource, { + ...config.options, + sorter: this._instantiationService.createInstance(OutlineTreeSorter, config.comparator, undefined), collapseByDefault: true, expandOnlyOnTwistieClick: true, multipleSelectionSupport: false, - sorter: this._outlineComparator, - identityProvider: new OutlineIdentityProvider(), - keyboardNavigationLabelProvider: new OutlineNavigationLabelProvider(), - accessibilityProvider: new OutlineAccessibilityProvider(localize('breadcrumbs', "Breadcrumbs")), - filter: this._instantiationService.createInstance(OutlineFilter, 'breadcrumbs') } ); } - dispose(): void { - this._symbolSortOrder.dispose(); - super.dispose(); - } + protected _setInput(input: OutlineElement2): Promise { - protected _setInput(input: BreadcrumbElement): Promise { - const element = input as TreeElement; - const model = OutlineModel.get(element)!; - const tree = this._tree as WorkbenchDataTree; + const viewState = input.outline.captureViewState(); + this.restoreViewState = () => { viewState.dispose(); }; - const overrideConfiguration = { - resource: model.uri, - overrideIdentifier: this._modeService.getModeIdByFilepathOrFirstLine(model.uri) - }; - this._outlineComparator.type = this._getOutlineItemCompareType(overrideConfiguration); + const tree = this._tree as WorkbenchDataTree, any, FuzzyScore>; - tree.setInput(model); - if (element !== model) { - tree.reveal(element, 0.5); - tree.setFocus([element], this._fakeEvent); + tree.setInput(input.outline); + if (input.element !== input.outline) { + tree.reveal(input.element, 0.5); + tree.setFocus([input.element], this._fakeEvent); } tree.domFocus(); return Promise.resolve(); } - protected _getTargetFromEvent(element: any): any | undefined { - if (element instanceof OutlineElement) { - return element; - } + protected _previewElement(element: any): IDisposable { + const outline: IOutline = this._tree.getInput(); + return outline.preview(element); } - private _getOutlineItemCompareType(overrideConfiguration?: IConfigurationOverrides): OutlineSortOrder { - switch (this._symbolSortOrder.getValue(overrideConfiguration)) { - case 'name': - return OutlineSortOrder.ByName; - case 'type': - return OutlineSortOrder.ByKind; - case 'position': - default: - return OutlineSortOrder.ByPosition; - } + async _revealElement(element: any, options: IEditorOptions, sideBySide: boolean): Promise { + this._onWillPickElement.fire(); + const outline: IOutline = this._tree.getInput(); + await outline.reveal(element, options, sideBySide); + return true; } } diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index fdb0a57ac..5fdcc208c 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -33,7 +33,7 @@ import { JoinAllGroupsAction, FocusLeftGroup, FocusAboveGroup, FocusRightGroup, FocusBelowGroup, EditorLayoutSingleAction, EditorLayoutTwoColumnsAction, EditorLayoutThreeColumnsAction, EditorLayoutTwoByTwoGridAction, EditorLayoutTwoRowsAction, EditorLayoutThreeRowsAction, EditorLayoutTwoColumnsBottomAction, EditorLayoutTwoRowsRightAction, NewEditorGroupLeftAction, NewEditorGroupRightAction, NewEditorGroupAboveAction, NewEditorGroupBelowAction, SplitEditorOrthogonalAction, CloseEditorInAllGroupsAction, NavigateToLastEditLocationAction, ToggleGroupSizesAction, ShowAllEditorsByMostRecentlyUsedAction, - QuickAccessPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickAccessLeastRecentlyUsedEditorAction, QuickAccessLeastRecentlyUsedEditorInGroupAction, ReopenResourcesAction, ToggleEditorTypeAction + QuickAccessPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickAccessLeastRecentlyUsedEditorAction, QuickAccessLeastRecentlyUsedEditorInGroupAction, ReopenResourcesAction, ToggleEditorTypeAction, DuplicateGroupDownAction, DuplicateGroupLeftAction, DuplicateGroupRightAction, DuplicateGroupUpAction } from 'vs/workbench/browser/parts/editor/editorActions'; import * as editorCommands from 'vs/workbench/browser/parts/editor/editorCommands'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -42,7 +42,7 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { isMacintosh } from 'vs/base/common/platform'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { OpenWorkspaceButtonContribution } from 'vs/workbench/browser/parts/editor/editorWidgets'; +import { OpenWorkspaceButtonContribution } from 'vs/workbench/browser/codeeditor'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { toLocalResource } from 'vs/base/common/resources'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; @@ -351,6 +351,10 @@ registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupLeftAction, registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupRightAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.RightArrow) }), 'View: Move Editor Group Right', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupUpAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.UpArrow) }), 'View: Move Editor Group Up', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupDownAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.DownArrow) }), 'View: Move Editor Group Down', CATEGORIES.View.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupLeftAction), 'View: Duplicate Editor Group Left', CATEGORIES.View.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupRightAction), 'View: Duplicate Editor Group Right', CATEGORIES.View.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupUpAction), 'View: Duplicate Editor Group Up', CATEGORIES.View.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupDownAction), 'View: Duplicate Editor Group Down', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveEditorToPreviousGroupAction, { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.LeftArrow, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.LeftArrow } }), 'View: Move Editor into Previous Group', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveEditorToNextGroupAction, { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.RightArrow, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.RightArrow } }), 'View: Move Editor into Next Group', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveEditorToFirstGroupAction, { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_1, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_1 } }), 'View: Move Editor into First Group', CATEGORIES.View.value); diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 8b18d4e90..7e901d0ae 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -23,6 +23,7 @@ import { AllEditorsByMostRecentlyUsedQuickAccess, ActiveGroupEditorsByMostRecent import { Codicon } from 'vs/base/common/codicons'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { openEditorWith, getAllAvailableEditors } from 'vs/workbench/services/editor/common/editorOpenWith'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class ExecuteCommandAction extends Action { @@ -746,12 +747,13 @@ export class CloseEditorInAllGroupsAction extends Action { } } -export class BaseMoveGroupAction extends Action { +class BaseMoveCopyGroupAction extends Action { constructor( id: string, label: string, private direction: GroupDirection, + private isMove: boolean, private editorGroupService: IEditorGroupsService ) { super(id, label); @@ -766,9 +768,18 @@ export class BaseMoveGroupAction extends Action { } if (sourceGroup) { - const targetGroup = this.findTargetGroup(sourceGroup); - if (targetGroup) { - this.editorGroupService.moveGroup(sourceGroup, targetGroup, this.direction); + let resultGroup: IEditorGroup | undefined = undefined; + if (this.isMove) { + const targetGroup = this.findTargetGroup(sourceGroup); + if (targetGroup) { + resultGroup = this.editorGroupService.moveGroup(sourceGroup, targetGroup, this.direction); + } + } else { + resultGroup = this.editorGroupService.copyGroup(sourceGroup, sourceGroup, this.direction); + } + + if (resultGroup) { + this.editorGroupService.activateGroup(resultGroup); } } } @@ -801,6 +812,18 @@ export class BaseMoveGroupAction extends Action { } } +class BaseMoveGroupAction extends BaseMoveCopyGroupAction { + + constructor( + id: string, + label: string, + direction: GroupDirection, + editorGroupService: IEditorGroupsService + ) { + super(id, label, direction, true, editorGroupService); + } +} + export class MoveGroupLeftAction extends BaseMoveGroupAction { static readonly ID = 'workbench.action.moveActiveEditorGroupLeft'; @@ -857,6 +880,74 @@ export class MoveGroupDownAction extends BaseMoveGroupAction { } } +class BaseDuplicateGroupAction extends BaseMoveCopyGroupAction { + + constructor( + id: string, + label: string, + direction: GroupDirection, + editorGroupService: IEditorGroupsService + ) { + super(id, label, direction, false, editorGroupService); + } +} + +export class DuplicateGroupLeftAction extends BaseDuplicateGroupAction { + + static readonly ID = 'workbench.action.duplicateActiveEditorGroupLeft'; + static readonly LABEL = nls.localize('duplicateActiveGroupLeft', "Duplicate Editor Group Left"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService + ) { + super(id, label, GroupDirection.LEFT, editorGroupService); + } +} + +export class DuplicateGroupRightAction extends BaseDuplicateGroupAction { + + static readonly ID = 'workbench.action.duplicateActiveEditorGroupRight'; + static readonly LABEL = nls.localize('duplicateActiveGroupRight', "Duplicate Editor Group Right"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService + ) { + super(id, label, GroupDirection.RIGHT, editorGroupService); + } +} + +export class DuplicateGroupUpAction extends BaseDuplicateGroupAction { + + static readonly ID = 'workbench.action.duplicateActiveEditorGroupUp'; + static readonly LABEL = nls.localize('duplicateActiveGroupUp', "Duplicate Editor Group Up"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService + ) { + super(id, label, GroupDirection.UP, editorGroupService); + } +} + +export class DuplicateGroupDownAction extends BaseDuplicateGroupAction { + + static readonly ID = 'workbench.action.duplicateActiveEditorGroupDown'; + static readonly LABEL = nls.localize('duplicateActiveGroupDown', "Duplicate Editor Group Down"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService + ) { + super(id, label, GroupDirection.DOWN, editorGroupService); + } +} + export class MinimizeOtherGroupsAction extends Action { static readonly ID = 'workbench.action.minimizeOtherEditors'; @@ -1803,9 +1894,8 @@ export class ReopenResourcesAction extends Action { constructor( id: string, label: string, - @IQuickInputService private readonly quickInputService: IQuickInputService, @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(id, label); } @@ -1823,7 +1913,7 @@ export class ReopenResourcesAction extends Action { const options = activeEditorPane.options; const group = activeEditorPane.group; - await openEditorWith(activeInput, undefined, options, group, this.editorService, this.configurationService, this.quickInputService); + await this.instantiationService.invokeFunction(openEditorWith, activeInput, undefined, options, group); } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index dcece5343..f9fd2d571 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -23,7 +23,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/browser/parts/editor/editorQuickAccess'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith'; @@ -484,7 +483,6 @@ function registerOpenEditorAPICommands(): void { const editorService = accessor.get(IEditorService); const editorGroupsService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); - const quickInputService = accessor.get(IQuickInputService); const [columnArg, optionsArg] = columnAndOptions ?? []; let group: IEditorGroup | undefined = undefined; @@ -504,7 +502,7 @@ function registerOpenEditorAPICommands(): void { const textOptions: ITextEditorOptions = optionsArg ? { ...optionsArg, override: false } : { override: false }; const input = editorService.createEditorInput({ resource: URI.revive(resource) }); - return openEditorWith(input, id, textOptions, group, editorService, configurationService, quickInputService); + return openEditorWith(accessor, input, id, textOptions, group); }); } @@ -902,24 +900,10 @@ function registerOtherEditorCommands(): void { id: TOGGLE_KEEP_EDITORS_COMMAND_ID, handler: accessor => { const configurationService = accessor.get(IConfigurationService); - const notificationService = accessor.get(INotificationService); - const openerService = accessor.get(IOpenerService); - // Update setting const currentSetting = configurationService.getValue('workbench.editor.enablePreview'); const newSetting = currentSetting === true ? false : true; configurationService.updateValue('workbench.editor.enablePreview', newSetting); - - // Inform user - notificationService.prompt( - Severity.Info, - newSetting ? - nls.localize('enablePreview', "Preview editors have been enabled in settings.") : - nls.localize('disablePreview', "Preview editors have been disabled in settings."), - [{ - label: nls.localize('learnMode', "Learn More"), run: () => openerService.open('https://go.microsoft.com/fwlink/?linkid=2147473') - }] - ); } }); diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 50f571563..d3d2258f5 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -283,7 +283,8 @@ class DropOverlay extends Themable { // Open in target group const options = getActiveTextEditorOptions(sourceGroup, draggedEditor.editor, EditorOptions.create({ pinned: true, // always pin dropped editor - sticky: sourceGroup.isSticky(draggedEditor.editor) // preserve sticky state + sticky: sourceGroup.isSticky(draggedEditor.editor), // preserve sticky state + override: false, // Use `draggedEditor.editor` as is. If it is already a custom editor, it will stay so. })); const copyEditor = this.isCopyOperation(event, draggedEditor); targetGroup.openEditor(draggedEditor.editor, options, copyEditor ? OpenEditorContext.COPY_EDITOR : OpenEditorContext.MOVE_EDITOR); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index a212456dd..277f029c3 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -322,8 +322,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private createContainerContextMenu(): void { const menu = this._register(this.menuService.createMenu(MenuId.EmptyEditorGroupContext, this.contextKeyService)); - this._register(addDisposableListener(this.element, EventType.CONTEXT_MENU, event => this.onShowContainerContextMenu(menu, event))); - this._register(addDisposableListener(this.element, TouchEventType.Contextmenu, event => this.onShowContainerContextMenu(menu))); + this._register(addDisposableListener(this.element, EventType.CONTEXT_MENU, e => this.onShowContainerContextMenu(menu, e))); + this._register(addDisposableListener(this.element, TouchEventType.Contextmenu, () => this.onShowContainerContextMenu(menu))); } private onShowContainerContextMenu(menu: IMenu, e?: MouseEvent): void { @@ -1704,13 +1704,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { layout(width: number, height: number): void { this.dimension = new Dimension(width, height); - // Ensure editor container gets height as CSS depending on the preferred height of the title control - const titleHeight = this.titleDimensions.height; - const editorHeight = Math.max(0, height - titleHeight); - this.editorContainer.style.height = `${editorHeight}px`; + // Layout the title area first to receive the size it occupies + const titleAreaSize = this.titleAreaControl.layout({ + container: this.dimension, + available: new Dimension(width, height - this.editorControl.minimumHeight) + }); - // Forward to controls - this.titleAreaControl.layout(new Dimension(width, titleHeight)); + // Pass the container width and remaining height to the editor layout + const editorHeight = Math.max(0, height - titleAreaSize.height); + this.editorContainer.style.height = `${editorHeight}px`; this.editorControl.layout(new Dimension(width, editorHeight)); } @@ -1769,7 +1771,7 @@ export interface EditorReplacement { readonly options?: EditorOptions; } -registerThemingParticipant((theme, collector, environment) => { +registerThemingParticipant((theme, collector) => { // Letterpress const letterpress = `./media/letterpress${theme.type === 'dark' ? '-dark' : theme.type === 'hc' ? '-hc' : ''}.svg`; diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 1d85f0ad5..3d289709b 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/editorstatus'; -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; import { format, compare, splitLines } from 'vs/base/common/strings'; import { extname, basename, isEqual } from 'vs/base/common/resources'; @@ -283,14 +283,15 @@ class State { } } -const nlsSingleSelectionRange = nls.localize('singleSelectionRange', "Ln {0}, Col {1} ({2} selected)"); -const nlsSingleSelection = nls.localize('singleSelection', "Ln {0}, Col {1}"); -const nlsMultiSelectionRange = nls.localize('multiSelectionRange', "{0} selections ({1} characters selected)"); -const nlsMultiSelection = nls.localize('multiSelection', "{0} selections"); -const nlsEOLLF = nls.localize('endOfLineLineFeed', "LF"); -const nlsEOLCRLF = nls.localize('endOfLineCarriageReturnLineFeed', "CRLF"); +const nlsSingleSelectionRange = localize('singleSelectionRange', "Ln {0}, Col {1} ({2} selected)"); +const nlsSingleSelection = localize('singleSelection', "Ln {0}, Col {1}"); +const nlsMultiSelectionRange = localize('multiSelectionRange', "{0} selections ({1} characters selected)"); +const nlsMultiSelection = localize('multiSelection', "{0} selections"); +const nlsEOLLF = localize('endOfLineLineFeed', "LF"); +const nlsEOLCRLF = localize('endOfLineCarriageReturnLineFeed', "CRLF"); export class EditorStatus extends Disposable implements IWorkbenchContribution { + private readonly tabFocusModeElement = this._register(new MutableDisposable()); private readonly columnSelectionModeElement = this._register(new MutableDisposable()); private readonly screenRedearModeElement = this._register(new MutableDisposable()); @@ -342,14 +343,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (!this.screenReaderNotification) { this.screenReaderNotification = this.notificationService.prompt( Severity.Info, - nls.localize('screenReaderDetectedExplanation.question', "Are you using a screen reader to operate VS Code? (word wrap is disabled when using a screen reader)"), + localize('screenReaderDetectedExplanation.question', "Are you using a screen reader to operate VS Code? (word wrap is disabled when using a screen reader)"), [{ - label: nls.localize('screenReaderDetectedExplanation.answerYes', "Yes"), + label: localize('screenReaderDetectedExplanation.answerYes', "Yes"), run: () => { this.configurationService.updateValue('editor.accessibilitySupport', 'on'); } }, { - label: nls.localize('screenReaderDetectedExplanation.answerNo', "No"), + label: localize('screenReaderDetectedExplanation.answerNo', "No"), run: () => { this.configurationService.updateValue('editor.accessibilitySupport', 'off'); } @@ -364,11 +365,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private async showIndentationPicker(): Promise { const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl); if (!activeTextEditorControl) { - return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + return this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); } if (this.editorService.activeEditor?.isReadonly()) { - return this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]); + return this.quickInputService.pick([{ label: localize('noWritableCodeEditor', "The active code editor is read-only.") }]); } const picks: QuickPickInput[] = [ @@ -390,25 +391,25 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { }; }); - picks.splice(3, 0, { type: 'separator', label: nls.localize('indentConvert', "convert file") }); - picks.unshift({ type: 'separator', label: nls.localize('indentView', "change view") }); + picks.splice(3, 0, { type: 'separator', label: localize('indentConvert', "convert file") }); + picks.unshift({ type: 'separator', label: localize('indentView', "change view") }); - const action = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }); + const action = await this.quickInputService.pick(picks, { placeHolder: localize('pickAction', "Select Action"), matchOnDetail: true }); return action?.run(); } private updateTabFocusModeElement(visible: boolean): void { if (visible) { if (!this.tabFocusModeElement.value) { - const text = nls.localize('tabFocusModeEnabled', "Tab Moves Focus"); + const text = localize('tabFocusModeEnabled', "Tab Moves Focus"); this.tabFocusModeElement.value = this.statusbarService.addEntry({ text, ariaLabel: text, - tooltip: nls.localize('disableTabMode', "Disable Accessibility Mode"), + tooltip: localize('disableTabMode', "Disable Accessibility Mode"), command: 'editor.action.toggleTabFocusMode', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.tabFocusMode', nls.localize('status.editor.tabFocusMode', "Accessibility Mode"), StatusbarAlignment.RIGHT, 100.7); + }, 'status.editor.tabFocusMode', localize('status.editor.tabFocusMode', "Accessibility Mode"), StatusbarAlignment.RIGHT, 100.7); } } else { this.tabFocusModeElement.clear(); @@ -418,15 +419,15 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private updateColumnSelectionModeElement(visible: boolean): void { if (visible) { if (!this.columnSelectionModeElement.value) { - const text = nls.localize('columnSelectionModeEnabled', "Column Selection"); + const text = localize('columnSelectionModeEnabled', "Column Selection"); this.columnSelectionModeElement.value = this.statusbarService.addEntry({ text, ariaLabel: text, - tooltip: nls.localize('disableColumnSelectionMode', "Disable Column Selection Mode"), + tooltip: localize('disableColumnSelectionMode', "Disable Column Selection Mode"), command: 'editor.action.toggleColumnSelection', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.columnSelectionMode', nls.localize('status.editor.columnSelectionMode', "Column Selection Mode"), StatusbarAlignment.RIGHT, 100.8); + }, 'status.editor.columnSelectionMode', localize('status.editor.columnSelectionMode', "Column Selection Mode"), StatusbarAlignment.RIGHT, 100.8); } } else { this.columnSelectionModeElement.clear(); @@ -436,14 +437,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private updateScreenReaderModeElement(visible: boolean): void { if (visible) { if (!this.screenRedearModeElement.value) { - const text = nls.localize('screenReaderDetected', "Screen Reader Optimized"); + const text = localize('screenReaderDetected', "Screen Reader Optimized"); this.screenRedearModeElement.value = this.statusbarService.addEntry({ text, ariaLabel: text, command: 'showEditorScreenReaderNotification', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.screenReaderMode', nls.localize('status.editor.screenReaderMode', "Screen Reader Mode"), StatusbarAlignment.RIGHT, 100.6); + }, 'status.editor.screenReaderMode', localize('status.editor.screenReaderMode', "Screen Reader Mode"), StatusbarAlignment.RIGHT, 100.6); } } else { this.screenRedearModeElement.clear(); @@ -459,11 +460,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('gotoLine', "Go to Line/Column"), + tooltip: localize('gotoLine', "Go to Line/Column"), command: 'workbench.action.gotoLine' }; - this.updateElement(this.selectionElement, props, 'status.editor.selection', nls.localize('status.editor.selection', "Editor Selection"), StatusbarAlignment.RIGHT, 100.5); + this.updateElement(this.selectionElement, props, 'status.editor.selection', localize('status.editor.selection', "Editor Selection"), StatusbarAlignment.RIGHT, 100.5); } private updateIndentationElement(text: string | undefined): void { @@ -475,11 +476,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('selectIndentation', "Select Indentation"), + tooltip: localize('selectIndentation', "Select Indentation"), command: 'changeEditorIndentation' }; - this.updateElement(this.indentationElement, props, 'status.editor.indentation', nls.localize('status.editor.indentation', "Editor Indentation"), StatusbarAlignment.RIGHT, 100.4); + this.updateElement(this.indentationElement, props, 'status.editor.indentation', localize('status.editor.indentation', "Editor Indentation"), StatusbarAlignment.RIGHT, 100.4); } private updateEncodingElement(text: string | undefined): void { @@ -491,11 +492,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('selectEncoding', "Select Encoding"), + tooltip: localize('selectEncoding', "Select Encoding"), command: 'workbench.action.editor.changeEncoding' }; - this.updateElement(this.encodingElement, props, 'status.editor.encoding', nls.localize('status.editor.encoding', "Editor Encoding"), StatusbarAlignment.RIGHT, 100.3); + this.updateElement(this.encodingElement, props, 'status.editor.encoding', localize('status.editor.encoding', "Editor Encoding"), StatusbarAlignment.RIGHT, 100.3); } private updateEOLElement(text: string | undefined): void { @@ -507,11 +508,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('selectEOL', "Select End of Line Sequence"), + tooltip: localize('selectEOL', "Select End of Line Sequence"), command: 'workbench.action.editor.changeEOL' }; - this.updateElement(this.eolElement, props, 'status.editor.eol', nls.localize('status.editor.eol', "Editor End of Line"), StatusbarAlignment.RIGHT, 100.2); + this.updateElement(this.eolElement, props, 'status.editor.eol', localize('status.editor.eol', "Editor End of Line"), StatusbarAlignment.RIGHT, 100.2); } private updateModeElement(text: string | undefined): void { @@ -523,11 +524,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('selectLanguageMode', "Select Language Mode"), + tooltip: localize('selectLanguageMode', "Select Language Mode"), command: 'workbench.action.editor.changeLanguageMode' }; - this.updateElement(this.modeElement, props, 'status.editor.mode', nls.localize('status.editor.mode', "Editor Language"), StatusbarAlignment.RIGHT, 100.1); + this.updateElement(this.modeElement, props, 'status.editor.mode', localize('status.editor.mode', "Editor Language"), StatusbarAlignment.RIGHT, 100.1); } private updateMetadataElement(text: string | undefined): void { @@ -539,10 +540,10 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('fileInfo', "File Information") + tooltip: localize('fileInfo', "File Information") }; - this.updateElement(this.metadataElement, props, 'status.editor.info', nls.localize('status.editor.info', "File Information"), StatusbarAlignment.RIGHT, 100); + this.updateElement(this.metadataElement, props, 'status.editor.info', localize('status.editor.info', "File Information"), StatusbarAlignment.RIGHT, 100); } private updateElement(element: MutableDisposable, props: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: number) { @@ -730,8 +731,8 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const modelOpts = model.getOptions(); update.indentation = ( modelOpts.insertSpaces - ? nls.localize('spacesSize', "Spaces: {0}", modelOpts.indentSize) - : nls.localize({ key: 'tabSize', comment: ['Tab corresponds to the tab key'] }, "Tab Size: {0}", modelOpts.tabSize) + ? localize('spacesSize', "Spaces: {0}", modelOpts.indentSize) + : localize({ key: 'tabSize', comment: ['Tab corresponds to the tab key'] }, "Tab Size: {0}", modelOpts.tabSize) ); } } @@ -766,7 +767,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (editorWidget) { const screenReaderDetected = this.accessibilityService.isScreenReaderOptimized(); if (screenReaderDetected) { - const screenReaderConfiguration = this.configurationService.getValue('editor').accessibilitySupport; + const screenReaderConfiguration = this.configurationService.getValue('editor')?.accessibilitySupport; if (screenReaderConfiguration === 'auto') { if (!this.promptedScreenReader) { this.promptedScreenReader = true; @@ -921,7 +922,7 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { const line = splitLines(this.currentMarker.message)[0]; const text = `${this.getType(this.currentMarker)} ${line}`; if (!this.statusBarEntryAccessor.value) { - this.statusBarEntryAccessor.value = this.statusbarService.addEntry({ text: '', ariaLabel: '' }, 'statusbar.currentProblem', nls.localize('currentProblem', "Current Problem"), StatusbarAlignment.LEFT); + this.statusBarEntryAccessor.value = this.statusbarService.addEntry({ text: '', ariaLabel: '' }, 'statusbar.currentProblem', localize('currentProblem', "Current Problem"), StatusbarAlignment.LEFT); } this.statusBarEntryAccessor.value.update({ text, ariaLabel: text }); } else { @@ -934,9 +935,11 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { if (!currentMarker) { return true; } + if (!previousMarker) { return true; } + return IMarkerData.makeKey(previousMarker) !== IMarkerData.makeKey(currentMarker); } @@ -946,6 +949,7 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { case MarkerSeverity.Warning: return '$(warning)'; case MarkerSeverity.Info: return '$(info)'; } + return ''; } @@ -953,17 +957,21 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { if (!this.configurationService.getValue('problems.showCurrentInStatus')) { return null; } + if (!this.editor) { return null; } + const model = this.editor.getModel(); if (!model) { return null; } + const position = this.editor.getPosition(); if (!position) { return null; } + return this.markers.find(marker => Range.containsPosition(marker, position)) || null; } @@ -971,13 +979,16 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { if (!this.editor) { return; } + const model = this.editor.getModel(); if (!model) { return; } + if (model && !changedResources.some(r => isEqual(model.uri, r))) { return; } + this.updateMarkers(); } @@ -985,10 +996,12 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { if (!this.editor) { return; } + const model = this.editor.getModel(); if (!model) { return; } + if (model) { this.markers = this.markerService.read({ resource: model.uri, @@ -998,6 +1011,7 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { } else { this.markers = []; } + this.updateStatus(); } } @@ -1007,9 +1021,11 @@ function compareMarker(a: IMarker, b: IMarker): number { if (res === 0) { res = MarkerSeverity.compare(a.severity, b.severity); } + if (res === 0) { res = Range.compareRangesUsingStarts(a, b); } + return res; } @@ -1022,7 +1038,7 @@ export class ShowLanguageExtensionsAction extends Action { @ICommandService private readonly commandService: ICommandService, @IExtensionGalleryService galleryService: IExtensionGalleryService ) { - super(ShowLanguageExtensionsAction.ID, nls.localize('showLanguageExtensions', "Search Marketplace Extensions for '{0}'...", fileExtension)); + super(ShowLanguageExtensionsAction.ID, localize('showLanguageExtensions', "Search Marketplace Extensions for '{0}'...", fileExtension)); this.enabled = galleryService.isEnabled(); } @@ -1035,7 +1051,7 @@ export class ShowLanguageExtensionsAction extends Action { export class ChangeModeAction extends Action { static readonly ID = 'workbench.action.editor.changeLanguageMode'; - static readonly LABEL = nls.localize('changeMode', "Change Language Mode"); + static readonly LABEL = localize('changeMode', "Change Language Mode"); constructor( actionId: string, @@ -1054,14 +1070,14 @@ export class ChangeModeAction extends Action { async run(): Promise { const activeEditorPane = this.editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean } | undefined; - if (activeEditorPane?.isNotebookEditor) { + if (activeEditorPane?.isNotebookEditor) { // TODO@rebornix TODO@jrieken debt: https://github.com/microsoft/vscode/issues/114554 // it's inside notebook editor return this.commandService.executeCommand('notebook.cell.changeLanguage'); } const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl); if (!activeTextEditorControl) { - await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); return; } @@ -1085,22 +1101,24 @@ export class ChangeModeAction extends Action { const languages = this.modeService.getRegisteredLanguageNames(); const picks: QuickPickInput[] = languages.sort().map((lang, index) => { const modeId = this.modeService.getModeIdForLanguageName(lang.toLowerCase()) || 'unknown'; + const extensions = this.modeService.getExtensions(lang).join(' '); let description: string; if (currentLanguageId === lang) { - description = nls.localize('languageDescription', "({0}) - Configured Language", modeId); + description = localize('languageDescription', "({0}) - Configured Language", modeId); } else { - description = nls.localize('languageDescriptionConfigured', "({0})", modeId); + description = localize('languageDescriptionConfigured', "({0})", modeId); } return { label: lang, + meta: extensions, iconClasses: getIconClassesForModeId(modeId), description }; }); if (hasLanguageSupport) { - picks.unshift({ type: 'separator', label: nls.localize('languagesPicks', "languages (identifier)") }); + picks.unshift({ type: 'separator', label: localize('languagesPicks', "languages (identifier)") }); } // Offer action to configure via settings @@ -1115,22 +1133,22 @@ export class ChangeModeAction extends Action { picks.unshift(galleryAction); } - configureModeSettings = { label: nls.localize('configureModeSettings', "Configure '{0}' language based settings...", currentLanguageId) }; + configureModeSettings = { label: localize('configureModeSettings', "Configure '{0}' language based settings...", currentLanguageId) }; picks.unshift(configureModeSettings); - configureModeAssociations = { label: nls.localize('configureAssociationsExt', "Configure File Association for '{0}'...", ext) }; + configureModeAssociations = { label: localize('configureAssociationsExt', "Configure File Association for '{0}'...", ext) }; picks.unshift(configureModeAssociations); } // Offer to "Auto Detect" const autoDetectMode: IQuickPickItem = { - label: nls.localize('autoDetect', "Auto Detect") + label: localize('autoDetect', "Auto Detect") }; if (hasLanguageSupport) { picks.unshift(autoDetectMode); } - const pick = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }); + const pick = await this.quickInputService.pick(picks, { placeHolder: localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }); if (!pick) { return; } @@ -1178,6 +1196,8 @@ export class ChangeModeAction extends Action { modeSupport.setMode(languageSelection.languageIdentifier.language); } } + + activeTextEditorControl.focus(); } } @@ -1194,12 +1214,12 @@ export class ChangeModeAction extends Action { id, label: lang, iconClasses: getIconClassesForModeId(id), - description: (id === currentAssociation) ? nls.localize('currentAssociation', "Current Association") : undefined + description: (id === currentAssociation) ? localize('currentAssociation', "Current Association") : undefined }; }); setTimeout(async () => { - const language = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || base) }); + const language = await this.quickInputService.pick(picks, { placeHolder: localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || base) }); if (language) { const fileAssociationsConfig = this.configurationService.inspect<{}>(FILES_ASSOCIATIONS_CONFIG); @@ -1233,7 +1253,7 @@ export interface IChangeEOLEntry extends IQuickPickItem { export class ChangeEOLAction extends Action { static readonly ID = 'workbench.action.editor.changeEOL'; - static readonly LABEL = nls.localize('changeEndOfLine', "Change End of Line Sequence"); + static readonly LABEL = localize('changeEndOfLine', "Change End of Line Sequence"); constructor( actionId: string, @@ -1247,12 +1267,12 @@ export class ChangeEOLAction extends Action { async run(): Promise { const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl); if (!activeTextEditorControl) { - await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); return; } if (this.editorService.activeEditor?.isReadonly()) { - await this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]); + await this.quickInputService.pick([{ label: localize('noWritableCodeEditor', "The active code editor is read-only.") }]); return; } @@ -1265,7 +1285,7 @@ export class ChangeEOLAction extends Action { const selectedIndex = (textModel?.getEOL() === '\n') ? 0 : 1; - const eol = await this.quickInputService.pick(EOLOptions, { placeHolder: nls.localize('pickEndOfLine', "Select End of Line Sequence"), activeItem: EOLOptions[selectedIndex] }); + const eol = await this.quickInputService.pick(EOLOptions, { placeHolder: localize('pickEndOfLine', "Select End of Line Sequence"), activeItem: EOLOptions[selectedIndex] }); if (eol) { const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorControl); if (activeCodeEditor?.hasModel() && !this.editorService.activeEditor?.isReadonly()) { @@ -1275,13 +1295,15 @@ export class ChangeEOLAction extends Action { textModel.pushStackElement(); } } + + activeTextEditorControl.focus(); } } export class ChangeEncodingAction extends Action { static readonly ID = 'workbench.action.editor.changeEncoding'; - static readonly LABEL = nls.localize('changeEncoding', "Change File Encoding"); + static readonly LABEL = localize('changeEncoding', "Change File Encoding"); constructor( actionId: string, @@ -1296,25 +1318,26 @@ export class ChangeEncodingAction extends Action { } async run(): Promise { - if (!getCodeEditor(this.editorService.activeTextEditorControl)) { - await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl); + if (!activeTextEditorControl) { + await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); return; } const activeEditorPane = this.editorService.activeEditorPane; if (!activeEditorPane) { - await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); return; } const encodingSupport: IEncodingSupport | null = toEditorWithEncodingSupport(activeEditorPane.input); if (!encodingSupport) { - await this.quickInputService.pick([{ label: nls.localize('noFileEditor', "No file active at this time") }]); + await this.quickInputService.pick([{ label: localize('noFileEditor', "No file active at this time") }]); return; } - const saveWithEncodingPick: IQuickPickItem = { label: nls.localize('saveWithEncoding', "Save with Encoding") }; - const reopenWithEncodingPick: IQuickPickItem = { label: nls.localize('reopenWithEncoding', "Reopen with Encoding") }; + const saveWithEncodingPick: IQuickPickItem = { label: localize('saveWithEncoding', "Save with Encoding") }; + const reopenWithEncodingPick: IQuickPickItem = { label: localize('reopenWithEncoding', "Reopen with Encoding") }; if (!Language.isDefaultVariant()) { const saveWithEncodingAlias = 'Save with Encoding'; @@ -1334,7 +1357,7 @@ export class ChangeEncodingAction extends Action { } else if (activeEditorPane.input.isReadonly()) { action = reopenWithEncodingPick; } else { - action = await this.quickInputService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }); + action = await this.quickInputService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: localize('pickAction', "Select Action"), matchOnDetail: true }); } if (!action) { @@ -1394,11 +1417,11 @@ export class ChangeEncodingAction extends Action { // If we have a guessed encoding, show it first unless it matches the configured encoding if (guessedEncoding && configuredEncoding !== guessedEncoding && SUPPORTED_ENCODINGS[guessedEncoding]) { picks.unshift({ type: 'separator' }); - picks.unshift({ id: guessedEncoding, label: SUPPORTED_ENCODINGS[guessedEncoding].labelLong, description: nls.localize('guessedEncoding', "Guessed from content") }); + picks.unshift({ id: guessedEncoding, label: SUPPORTED_ENCODINGS[guessedEncoding].labelLong, description: localize('guessedEncoding', "Guessed from content") }); } const encoding = await this.quickInputService.pick(picks, { - placeHolder: isReopenWithEncoding ? nls.localize('pickEncodingForReopen', "Select File Encoding to Reopen File") : nls.localize('pickEncodingForSave', "Select File Encoding to Save with"), + placeHolder: isReopenWithEncoding ? localize('pickEncodingForReopen', "Select File Encoding to Reopen File") : localize('pickEncodingForSave', "Select File Encoding to Save with"), activeItem: items[typeof directMatchIndex === 'number' ? directMatchIndex : typeof aliasMatchIndex === 'number' ? aliasMatchIndex : -1] }); @@ -1414,5 +1437,7 @@ export class ChangeEncodingAction extends Action { if (typeof encoding.id !== 'undefined' && activeEncodingSupport && activeEncodingSupport.getEncoding() !== encoding.id) { activeEncodingSupport.setEncoding(encoding.id, isReopenWithEncoding ? EncodingMode.Decode : EncodingMode.Encode); // Set new encoding } + + activeTextEditorControl.focus(); } } diff --git a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css index 77a7fa03e..366f6dbe9 100644 --- a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css @@ -14,7 +14,7 @@ flex: auto; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .title-label { +.monaco-workbench .part.editor > .content .editor-group-container > .title > .label-container > .title-label { line-height: 35px; overflow: hidden; text-overflow: ellipsis; @@ -22,16 +22,16 @@ padding-left: 20px; } -.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .no-tabs.title-label { - flex: none; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title .monaco-icon-label::before { - height: 35px; /* tweak the icon size of the editor labels when icons are enabled */ +.monaco-workbench .part.editor > .content .editor-group-container > .title > .label-container > .title-label > .monaco-icon-label-container { + flex: none; /* helps to show decorations right next to the label and not at the end */ } /* Breadcrumbs */ +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .no-tabs.title-label { + flex: none; +} + .monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control { flex: 1 50%; overflow: hidden; @@ -62,13 +62,11 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item.root_folder + .monaco-breadcrumb-item::before, .monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control.relative-path .monaco-breadcrumb-item:nth-child(2)::before, .monaco-workbench.windows .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:nth-child(2)::before { - /* workspace folder, item following workspace folder, or relative path -> hide first seperator */ - display: none; + display: none; /* workspace folder, item following workspace folder, or relative path -> hide first seperator */ } .monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item.root_folder::after { - /* use dot separator for workspace folder */ - content: '\00a0•\00a0'; + content: '\00a0•\00a0'; /* use dot separator for workspace folder */ padding: 0; } @@ -80,13 +78,18 @@ padding: 0 1px; } -.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item .codicon:last-child, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child .codicon:last-child { - display: none; /* hides chevrons when no tabs visible and when last items */ +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item .codicon:last-child { + display: none; /* hides chevrons when no tabs visible */ } -/* Title Actions */ -.monaco-workbench .part.editor > .content .editor-group-container > .title .title-actions { +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-icon-label::before { + height: 18px; + padding-right: 2px; +} + +/* Editor Actions Toolbar (via title actions) */ + +.monaco-workbench .part.editor > .content .editor-group-container > .title > .title-actions { display: flex; flex: initial; opacity: 0.5; @@ -94,6 +97,6 @@ height: 35px; } -.monaco-workbench .part.editor > .content .editor-group-container.active > .title .title-actions { +.monaco-workbench .part.editor > .content .editor-group-container.active > .title > .title-actions { opacity: 1; } diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css index a9158b796..4dd7e77d5 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -3,17 +3,45 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* + ################################### z-index explainer ################################### + + Tabs have various levels of z-index depending on state, typically: + - scrollbar should be above all + - sticky (compact, shrink) tabs need to be above non-sticky tabs for scroll under effect + including non-sticky tabs-top borders, otherwise these borders would not scroll under + (https://github.com/microsoft/vscode/issues/111641) + - bottom-border needs to be above tabs bottom border to win but also support sticky tabs + (https://github.com/microsoft/vscode/issues/99084) <- this currently cannot be done and + is still broken. putting sticky-tabs above tabs bottom border would not render this + border at all for sticky tabs. + + On top of that there is 2 borders with a z-index for a general border below tabs + - tabs bottom border + - editor title bottom border (when breadcrumbs are disabled, this border will appear + same as tabs bottom border) + + The following tabls shows the current stacking order: + + [z-index] [kind] + 7 scrollbar + 6 active-tab border-bottom + 5 tabs, title border bottom + 4 sticky-tab + 2 active/dirty-tab border top + 0 tab + + ########################################################################################## +*/ + /* Title Container */ -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container { display: flex; + position: relative; /* position tabs border bottom or editor actions (when tabs wrap) relative to this container */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.tabs-border-bottom { - position: relative; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.tabs-border-bottom::after { +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.tabs-border-bottom::after { content: ''; position: absolute; bottom: 0; @@ -25,12 +53,12 @@ height: 1px; } -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container > .monaco-scrollable-element { +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container > .monaco-scrollable-element { flex: 1; } -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container > .monaco-scrollable-element .scrollbar { - z-index: 3; /* on top of tabs */ +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container > .monaco-scrollable-element .scrollbar { + z-index: 7; cursor: default; } @@ -46,6 +74,13 @@ overflow: scroll !important; } +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container { + + /* Enable wrapping via flex layout and dynamic height */ + height: auto; + flex-wrap: wrap; +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container::-webkit-scrollbar { display: none; /* Chrome + Safari: hide scrollbar */ } @@ -62,6 +97,10 @@ padding-left: 10px; } +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab:last-child { + margin-right: var(--last-tab-margin-right); /* when tabs wrap, we need a margin away from the absolute positioned editor actions */ +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.tab-actions-right, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.tab-actions-off:not(.sticky-compact) { padding-left: 5px; /* reduce padding when we show icons and are in shrinking mode and tab actions is not left (unless sticky-compact) */ @@ -74,6 +113,10 @@ flex-shrink: 0; } +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab.sizing-fit { + flex-grow: 1; /* grow the tabs to fill each row for a more homogeneous look when tabs wrap */ +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink { min-width: 80px; flex-basis: 0; /* all tabs are even */ @@ -89,7 +132,7 @@ /** Sticky compact/shrink tabs do not scroll in case of overflow and are always above unsticky tabs which scroll under */ position: sticky; - z-index: 1; + z-index: 4; /** Sticky compact/shrink tabs are even and never grow */ flex-basis: 0; @@ -118,9 +161,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky-compact, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-fit.sticky-shrink, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky-shrink { - - /** Disable sticky positions for sticky compact/shrink tabs if the available space is too little */ - position: static; + position: static; /** disable sticky positions for sticky compact/shrink tabs if the available space is too little */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-left .action-label { @@ -132,7 +173,7 @@ content: ''; display: flex; flex: 0; - width: 5px; /* Reserve space to hide tab fade when close button is left or off (fixes https://github.com/microsoft/vscode/issues/45728) */ + width: 5px; /* reserve space to hide tab fade when close button is left or off (fixes https://github.com/microsoft/vscode/issues/45728) */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.tab-actions-left { @@ -167,24 +208,26 @@ display: block; position: absolute; left: 0; - z-index: 6; /* over possible title border */ pointer-events: none; width: 100%; } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container { + z-index: 2; top: 0; height: 1px; background-color: var(--tab-border-top-color); } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container { + z-index: 6; bottom: 0; height: 1px; background-color: var(--tab-border-bottom-color); } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container { + z-index: 2; top: 0; height: 2px; background-color: var(--tab-dirty-border-top-color); @@ -203,13 +246,16 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .tab-label::after { - content: ''; + content: ''; /* enables a linear gradient to overlay the end of the label when tabs overflow */ position: absolute; right: 0; - height: 100%; width: 5px; opacity: 1; padding: 0; + /* the rules below ensure that the gradient does not impact top/bottom borders (https://github.com/microsoft/vscode/issues/115129) */ + top: 1px; + bottom: 1px; + height: calc(100% - 2px); } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink:focus > .tab-label::after { @@ -243,7 +289,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-shrink > .tab-actions { flex: 0; - overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink to make more room... */ + overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink to make more room */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.tab-actions-right.sizing-shrink > .tab-actions, @@ -301,7 +347,7 @@ margin-right: 0.5em; } -/* No Tab Actions */ +/* Tab Actions: Off */ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-off { padding-right: 10px; /* give a little bit more room if tab actions is off */ @@ -324,15 +370,6 @@ pointer-events: none; /* don't allow tab actions to be clicked when running without tab actions */ } -/* Editor Actions */ - -.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions { - cursor: default; - flex: initial; - padding: 0 8px 0 4px; - height: 35px; -} - /* Breadcrumbs */ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control { @@ -350,11 +387,17 @@ height: 22px; /* tweak the icon size of the editor labels when icons are enabled */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .outline-element-icon { + padding-right: 3px; + height: 22px; /* tweak the icon size of the editor labels when icons are enabled */ + line-height: 22px; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item { max-width: 80%; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before { width: 16px; height: 22px; display: flex; @@ -362,6 +405,27 @@ justify-content: center; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child { padding-right: 8px; } + +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child .codicon:last-child { + display: none; /* hides chevrons when last item */ +} + +/* Editor Actions Toolbar */ + +.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions { + cursor: default; + flex: initial; + padding: 0 8px 0 4px; + height: 35px; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .editor-actions { + + /* When tabs are wrapped, position the editor actions at the end of the very last row */ + position: absolute; + bottom: 0; + right: 0; +} diff --git a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css index 59ab0d4af..420f4031f 100644 --- a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css @@ -26,6 +26,15 @@ cursor: pointer; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .monaco-icon-label::before { + height: 35px; /* tweak the icon size of the editor labels when icons are enabled */ +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .monaco-icon-label::after, +.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .monaco-icon-label::after { + padding-right: 0; /* by default the icon label has a padding right and this isn't wanted when not showing tabs and not showing breadcrumbs */ +} + /* Title Actions */ .monaco-workbench .part.editor > .content .editor-group-container > .title .title-actions .action-label:not(span), @@ -59,13 +68,12 @@ opacity: 0.4; } -/* Drag Cursor */ +/* Drag and Drop */ + .monaco-workbench .part.editor > .content .editor-group-container > .title { cursor: grab; } -/* Drag and Drop Feedback */ - .monaco-editor-group-drag-image { display: inline-block; padding: 1px 7px; diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 66a7caec1..c779550c1 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/notabstitlecontrol'; import { EditorResourceAccessor, Verbosity, IEditorInput, IEditorPartOptions, SideBySideEditor } from 'vs/workbench/common/editor'; -import { TitleControl, IToolbarActions } from 'vs/workbench/browser/parts/editor/titleControl'; +import { TitleControl, IToolbarActions, ITitleControlDimensions } from 'vs/workbench/browser/parts/editor/titleControl'; import { ResourceLabel, IResourceLabel } from 'vs/workbench/browser/labels'; import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; @@ -15,6 +15,7 @@ import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/edito import { Color } from 'vs/base/common/color'; import { withNullAsUndefined, assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { IEditorGroupTitleDimensions } from 'vs/workbench/browser/parts/editor/editor'; +import { equals } from 'vs/base/common/objects'; interface IRenderedEditorLabel { editor?: IEditorInput; @@ -50,7 +51,7 @@ export class NoTabsTitleControl extends TitleControl { // Breadcrumbs this.createBreadcrumbsControl(labelContainer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, breadcrumbsBackground: () => Color.transparent }); titleContainer.classList.toggle('breadcrumbs', Boolean(this.breadcrumbsControl)); - this._register({ dispose: () => titleContainer.classList.remove('breadcrumbs') }); // import to remove because the container is a shared dom node + this._register({ dispose: () => titleContainer.classList.remove('breadcrumbs') }); // important to remove because the container is a shared dom node // Right Actions Container const actionsContainer = document.createElement('div'); @@ -67,16 +68,16 @@ export class NoTabsTitleControl extends TitleControl { this.enableGroupDragging(titleContainer); // Pin on double click - this._register(addDisposableListener(titleContainer, EventType.DBLCLICK, (e: MouseEvent) => this.onTitleDoubleClick(e))); + this._register(addDisposableListener(titleContainer, EventType.DBLCLICK, e => this.onTitleDoubleClick(e))); // Detect mouse click - this._register(addDisposableListener(titleContainer, EventType.AUXCLICK, (e: MouseEvent) => this.onTitleAuxClick(e))); + this._register(addDisposableListener(titleContainer, EventType.AUXCLICK, e => this.onTitleAuxClick(e))); // Detect touch this._register(addDisposableListener(titleContainer, TouchEventType.Tap, (e: GestureEvent) => this.onTitleTap(e))); // Context Menu - this._register(addDisposableListener(titleContainer, EventType.CONTEXT_MENU, (e: Event) => { + this._register(addDisposableListener(titleContainer, EventType.CONTEXT_MENU, e => { if (this.group.activeEditor) { this.onContextMenu(this.group.activeEditor, e, titleContainer); } @@ -188,7 +189,7 @@ export class NoTabsTitleControl extends TitleControl { } updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void { - if (oldOptions.labelFormat !== newOptions.labelFormat) { + if (oldOptions.labelFormat !== newOptions.labelFormat || !equals(oldOptions.decorations, newOptions.decorations)) { this.redraw(); } } @@ -236,6 +237,7 @@ export class NoTabsTitleControl extends TitleControl { private redraw(): void { const editor = withNullAsUndefined(this.group.activeEditor); + const options = this.accessor.partOptions; const isEditorPinned = editor ? this.group.isPinned(editor) : false; const isGroupActive = this.accessor.activeGroup === this.group; @@ -291,14 +293,18 @@ export class NoTabsTitleControl extends TitleControl { { title, italic: !isEditorPinned, - extraClasses: ['no-tabs', 'title-label'] + extraClasses: ['no-tabs', 'title-label'], + fileDecorations: { + colors: Boolean(options.decorations?.colors), + badges: Boolean(options.decorations?.badges) + }, } ); if (isGroupActive) { - editorLabel.element.style.color = this.getColor(TAB_ACTIVE_FOREGROUND) || ''; + titleContainer.style.color = this.getColor(TAB_ACTIVE_FOREGROUND) || ''; } else { - editorLabel.element.style.color = this.getColor(TAB_UNFOCUSED_ACTIVE_FOREGROUND) || ''; + titleContainer.style.color = this.getColor(TAB_UNFOCUSED_ACTIVE_FOREGROUND) || ''; } // Update Editor Actions Toolbar @@ -333,9 +339,11 @@ export class NoTabsTitleControl extends TitleControl { }; } - layout(dimension: Dimension): void { + layout(dimensions: ITitleControlDimensions): Dimension { if (this.breadcrumbsControl) { this.breadcrumbsControl.layout(undefined); } + + return new Dimension(dimensions.container.width, this.getDimensions().height); } } diff --git a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts deleted file mode 100644 index 222d0fd5e..000000000 --- a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts +++ /dev/null @@ -1,121 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { Emitter } from 'vs/base/common/event'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IRange } from 'vs/editor/common/core/range'; -import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { ICodeEditor, isCodeEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; -import { TrackedRangeStickiness, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; -import { isEqual } from 'vs/base/common/resources'; - -export interface IRangeHighlightDecoration { - resource: URI; - range: IRange; - isWholeLine?: boolean; -} - -export class RangeHighlightDecorations extends Disposable { - - private rangeHighlightDecorationId: string | null = null; - private editor: ICodeEditor | null = null; - private readonly editorDisposables = this._register(new DisposableStore()); - - private readonly _onHighlightRemoved: Emitter = this._register(new Emitter()); - readonly onHighlightRemoved = this._onHighlightRemoved.event; - - constructor( - @IEditorService private readonly editorService: IEditorService - ) { - super(); - } - - removeHighlightRange() { - if (this.editor && this.editor.getModel() && this.rangeHighlightDecorationId) { - this.editor.deltaDecorations([this.rangeHighlightDecorationId], []); - this._onHighlightRemoved.fire(); - } - - this.rangeHighlightDecorationId = null; - } - - highlightRange(range: IRangeHighlightDecoration, editor?: any) { - editor = editor ?? this.getEditor(range); - if (isCodeEditor(editor)) { - this.doHighlightRange(editor, range); - } else if (isCompositeEditor(editor) && isCodeEditor(editor.activeCodeEditor)) { - this.doHighlightRange(editor.activeCodeEditor, range); - } - } - - private doHighlightRange(editor: ICodeEditor, selectionRange: IRangeHighlightDecoration) { - this.removeHighlightRange(); - - editor.changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => { - this.rangeHighlightDecorationId = changeAccessor.addDecoration(selectionRange.range, this.createRangeHighlightDecoration(selectionRange.isWholeLine)); - }); - - this.setEditor(editor); - } - - private getEditor(resourceRange: IRangeHighlightDecoration): ICodeEditor | undefined { - const activeEditor = this.editorService.activeEditor; - const resource = activeEditor && activeEditor.resource; - if (resource && isEqual(resource, resourceRange.resource)) { - return this.editorService.activeTextEditorControl as ICodeEditor; - } - - return undefined; - } - - private setEditor(editor: ICodeEditor) { - if (this.editor !== editor) { - this.editorDisposables.clear(); - this.editor = editor; - this.editorDisposables.add(this.editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => { - if ( - e.reason === CursorChangeReason.NotSet - || e.reason === CursorChangeReason.Explicit - || e.reason === CursorChangeReason.Undo - || e.reason === CursorChangeReason.Redo - ) { - this.removeHighlightRange(); - } - })); - this.editorDisposables.add(this.editor.onDidChangeModel(() => { this.removeHighlightRange(); })); - this.editorDisposables.add(this.editor.onDidDispose(() => { - this.removeHighlightRange(); - this.editor = null; - })); - } - } - - private static readonly _WHOLE_LINE_RANGE_HIGHLIGHT = ModelDecorationOptions.register({ - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - className: 'rangeHighlight', - isWholeLine: true - }); - - private static readonly _RANGE_HIGHLIGHT = ModelDecorationOptions.register({ - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - className: 'rangeHighlight' - }); - - private createRangeHighlightDecoration(isWholeLine: boolean = true): ModelDecorationOptions { - return (isWholeLine ? RangeHighlightDecorations._WHOLE_LINE_RANGE_HIGHLIGHT : RangeHighlightDecorations._RANGE_HIGHLIGHT); - } - - dispose() { - super.dispose(); - - if (this.editor && this.editor.getModel()) { - this.removeHighlightRange(); - this.editor = null; - } - } -} diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index bcd835f4c..e6adbec28 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -18,19 +18,18 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IMenuService } from 'vs/platform/actions/common/actions'; -import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; +import { ITitleControlDimensions, TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IDisposable, dispose, DisposableStore, combinedDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { getOrSet } from 'vs/base/common/map'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER, TAB_LAST_PINNED_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, contrastBorder, editorBackground, breadcrumbsBackground } from 'vs/platform/theme/common/colorRegistry'; import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { MergeGroupMode, IMergeGroupOptions, GroupsArrangement, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; @@ -48,6 +47,7 @@ import { IPath, win32, posix } from 'vs/base/common/path'; import { insert } from 'vs/base/common/arrays'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { isSafari } from 'vs/base/browser/browser'; +import { equals } from 'vs/base/common/objects'; interface IEditorInputLabel { name?: string; @@ -73,6 +73,9 @@ export class TabsTitleControl extends TitleControl { private static readonly TAB_HEIGHT = 35; + private static readonly MOUSE_WHEEL_EVENT_THRESHOLD = 150; + private static readonly MOUSE_WHEEL_DISTANCE_THRESHOLD = 1.5; + private titleContainer: HTMLElement | undefined; private tabsAndActionsContainer: HTMLElement | undefined; private tabsContainer: HTMLElement | undefined; @@ -87,12 +90,18 @@ export class TabsTitleControl extends TitleControl { private tabActionBars: ActionBar[] = []; private tabDisposables: IDisposable[] = []; - private dimension: Dimension | undefined; + private dimensions: ITitleControlDimensions & { used?: Dimension } = { + container: Dimension.None, + available: Dimension.None + }; + private readonly layoutScheduled = this._register(new MutableDisposable()); private blockRevealActiveTab: boolean | undefined; private path: IPath = isWindows ? win32 : posix; + private lastMouseWheelEventTime = 0; + constructor( parent: HTMLElement, accessor: IEditorGroupsAccessor, @@ -106,19 +115,21 @@ export class TabsTitleControl extends TitleControl { @IMenuService menuService: IMenuService, @IQuickInputService quickInputService: IQuickInputService, @IThemeService themeService: IThemeService, - @IExtensionService extensionService: IExtensionService, @IConfigurationService configurationService: IConfigurationService, @IFileService fileService: IFileService, @IEditorService private readonly editorService: EditorServiceImpl, @IPathService private readonly pathService: IPathService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService ) { - super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickInputService, themeService, extensionService, configurationService, fileService); + super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickInputService, themeService, configurationService, fileService); // Resolve the correct path library for the OS we are on // If we are connected to remote, this accounts for the // remote OS. (async () => this.path = await this.pathService.path)(); + + // React to decorations changing for our resource labels + this._register(this.tabResourceLabels.onDidChangeDecorations(() => this.doHandleDecorationsChange())); } protected create(parent: HTMLElement): void { @@ -151,7 +162,7 @@ export class TabsTitleControl extends TitleControl { // Editor Actions Toolbar this.createEditorActionsToolBar(this.editorToolbarContainer); - // Breadcrumbs (are on a separate row below tabs and actions) + // Breadcrumbs const breadcrumbsContainer = document.createElement('div'); breadcrumbsContainer.classList.add('tabs-breadcrumbs'); this.titleContainer.appendChild(breadcrumbsContainer); @@ -242,7 +253,7 @@ export class TabsTitleControl extends TitleControl { }); // Prevent auto-scrolling (https://github.com/microsoft/vscode/issues/16690) - this._register(addDisposableListener(tabsContainer, EventType.MOUSE_DOWN, (e: MouseEvent) => { + this._register(addDisposableListener(tabsContainer, EventType.MOUSE_DOWN, e => { if (e.button === 1) { e.preventDefault(); } @@ -337,8 +348,27 @@ export class TabsTitleControl extends TitleControl { } } - // Figure out scrolling direction - const nextEditor = this.group.getEditorByIndex(this.group.getIndexOfEditor(activeEditor) + (e.deltaX < 0 || e.deltaY < 0 /* scrolling up */ ? -1 : 1)); + // Ignore event if the last one happened too recently (https://github.com/microsoft/vscode/issues/96409) + // The restriction is relaxed according to the absolute value of `deltaX` and `deltaY` + // to support discrete (mouse wheel) and contiguous scrolling (touchpad) equally well + const now = Date.now(); + if (now - this.lastMouseWheelEventTime < TabsTitleControl.MOUSE_WHEEL_EVENT_THRESHOLD - 2 * (Math.abs(e.deltaX) + Math.abs(e.deltaY))) { + return; + } + + this.lastMouseWheelEventTime = now; + + // Figure out scrolling direction but ignore it if too subtle + let tabSwitchDirection: number; + if (e.deltaX + e.deltaY < - TabsTitleControl.MOUSE_WHEEL_DISTANCE_THRESHOLD) { + tabSwitchDirection = -1; + } else if (e.deltaX + e.deltaY > TabsTitleControl.MOUSE_WHEEL_DISTANCE_THRESHOLD) { + tabSwitchDirection = 1; + } else { + return; + } + + const nextEditor = this.group.getEditorByIndex(this.group.getIndexOfEditor(activeEditor) + tabSwitchDirection); if (!nextEditor) { return; } @@ -351,12 +381,19 @@ export class TabsTitleControl extends TitleControl { })); } + private doHandleDecorationsChange(): void { + + // A change to decorations potentially has an impact on the size of tabs + // so we need to trigger a layout in that case to adjust things + this.layout(this.dimensions); + } + protected updateEditorActionsToolbar(): void { super.updateEditorActionsToolbar(); // Changing the actions in the toolbar can have an impact on the size of the // tab container, so we need to layout the tabs to make sure the active is visible - this.layout(this.dimension); + this.layout(this.dimensions); } openEditor(editor: IEditorInput): void { @@ -439,7 +476,7 @@ export class TabsTitleControl extends TitleControl { }); // Moving an editor requires a layout to keep the active editor visible - this.layout(this.dimension); + this.layout(this.dimensions); } pinEditor(editor: IEditorInput): void { @@ -466,7 +503,7 @@ export class TabsTitleControl extends TitleControl { }); // A change to the sticky state requires a layout to keep the active editor visible - this.layout(this.dimension); + this.layout(this.dimensions); } setActive(isGroupActive: boolean): void { @@ -478,7 +515,7 @@ export class TabsTitleControl extends TitleControl { // Activity has an impact on the toolbar, so we need to update and layout this.updateEditorActionsToolbar(); - this.layout(this.dimension); + this.layout(this.dimensions); } private updateEditorLabelAggregator = this._register(new RunOnceScheduler(() => this.updateEditorLabels(), 0)); @@ -504,7 +541,7 @@ export class TabsTitleControl extends TitleControl { }); // A change to a label requires a layout to keep the active editor visible - this.layout(this.dimension); + this.layout(this.dimensions); } updateEditorDirty(editor: IEditorInput): void { @@ -531,7 +568,9 @@ export class TabsTitleControl extends TitleControl { oldOptions.pinnedTabSizing !== newOptions.pinnedTabSizing || oldOptions.showIcons !== newOptions.showIcons || oldOptions.hasIcons !== newOptions.hasIcons || - oldOptions.highlightModifiedTabs !== newOptions.highlightModifiedTabs + oldOptions.highlightModifiedTabs !== newOptions.highlightModifiedTabs || + oldOptions.wrapTabs !== newOptions.wrapTabs || + !equals(oldOptions.decorations, newOptions.decorations) ) { this.redraw(); } @@ -648,7 +687,7 @@ export class TabsTitleControl extends TitleControl { }; // Open on Click / Touch - disposables.add(addDisposableListener(tab, EventType.MOUSE_DOWN, (e: MouseEvent) => handleClickOrTouch(e))); + disposables.add(addDisposableListener(tab, EventType.MOUSE_DOWN, e => handleClickOrTouch(e))); disposables.add(addDisposableListener(tab, TouchEventType.Tap, (e: GestureEvent) => handleClickOrTouch(e))); // Touch Scroll Support @@ -657,14 +696,14 @@ export class TabsTitleControl extends TitleControl { })); // Prevent flicker of focus outline on tab until editor got focus - disposables.add(addDisposableListener(tab, EventType.MOUSE_UP, (e: MouseEvent) => { + disposables.add(addDisposableListener(tab, EventType.MOUSE_UP, e => { EventHelper.stop(e); tab.blur(); })); // Close on mouse middle click - disposables.add(addDisposableListener(tab, EventType.AUXCLICK, (e: MouseEvent) => { + disposables.add(addDisposableListener(tab, EventType.AUXCLICK, e => { if (e.button === 1 /* Middle Button*/) { EventHelper.stop(e, true /* for https://github.com/microsoft/vscode/issues/56715 */); @@ -674,7 +713,7 @@ export class TabsTitleControl extends TitleControl { })); // Context menu on Shift+F10 - disposables.add(addDisposableListener(tab, EventType.KEY_DOWN, (e: KeyboardEvent) => { + disposables.add(addDisposableListener(tab, EventType.KEY_DOWN, e => { const event = new StandardKeyboardEvent(e); if (event.shiftKey && event.keyCode === KeyCode.F10) { showContextMenu(e); @@ -687,7 +726,7 @@ export class TabsTitleControl extends TitleControl { })); // Keyboard accessibility - disposables.add(addDisposableListener(tab, EventType.KEY_UP, (e: KeyboardEvent) => { + disposables.add(addDisposableListener(tab, EventType.KEY_UP, e => { const event = new StandardKeyboardEvent(e); let handled = false; @@ -750,7 +789,7 @@ export class TabsTitleControl extends TitleControl { }); // Context menu - disposables.add(addDisposableListener(tab, EventType.CONTEXT_MENU, (e: Event) => { + disposables.add(addDisposableListener(tab, EventType.CONTEXT_MENU, e => { EventHelper.stop(e, true); const input = this.group.getEditorByIndex(index); @@ -760,7 +799,7 @@ export class TabsTitleControl extends TitleControl { }, true /* use capture to fix https://github.com/microsoft/vscode/issues/19145 */)); // Drag support - disposables.add(addDisposableListener(tab, EventType.DRAG_START, (e: DragEvent) => { + disposables.add(addDisposableListener(tab, EventType.DRAG_START, e => { const editor = this.group.getEditorByIndex(index); if (!editor) { return; @@ -1022,7 +1061,7 @@ export class TabsTitleControl extends TitleControl { this.updateEditorActionsToolbar(); // Ensure the active tab is always revealed - this.layout(this.dimension); + this.layout(this.dimensions); } private redrawTab(editor: IEditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar): void { @@ -1092,12 +1131,14 @@ export class TabsTitleControl extends TitleControl { // or their first character of the name otherwise let name: string | undefined; let forceLabel = false; + let forceDisableBadgeDecorations = false; let description: string; if (options.pinnedTabSizing === 'compact' && this.group.isSticky(index)) { const isShowingIcons = options.showIcons && options.hasIcons; name = isShowingIcons ? '' : tabLabel.name?.charAt(0).toUpperCase(); description = ''; forceLabel = true; + forceDisableBadgeDecorations = true; // not enough space when sticky tabs are compact } else { name = tabLabel.name; description = tabLabel.description || ''; @@ -1116,7 +1157,16 @@ export class TabsTitleControl extends TitleControl { // Label tabLabelWidget.setResource( { name, description, resource: EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.BOTH }) }, - { title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor), forceLabel } + { + title, + extraClasses: ['tab-label'], + italic: !this.group.isPinned(editor), + forceLabel, + fileDecorations: { + colors: Boolean(options.decorations?.colors), + badges: forceDisableBadgeDecorations ? false : Boolean(options.decorations?.badges) + } + } ); // Tests helper @@ -1234,62 +1284,179 @@ export class TabsTitleControl extends TitleControl { } getDimensions(): IEditorGroupTitleDimensions { - let height = TabsTitleControl.TAB_HEIGHT; - if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { - height += BreadcrumbsControl.HEIGHT; + let height: number; + + // Wrap: we need to ask `offsetHeight` to get + // the real height of the title area with wrapping. + if (this.accessor.partOptions.wrapTabs && this.tabsAndActionsContainer?.classList.contains('wrapping')) { + height = this.tabsAndActionsContainer.offsetHeight; + } else { + height = TabsTitleControl.TAB_HEIGHT; } - return { - height, - offset: TabsTitleControl.TAB_HEIGHT - }; + const offset = height; + + // Account for breadcrumbs if visible + if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { + height += BreadcrumbsControl.HEIGHT; // Account for breadcrumbs if visible + } + + return { height, offset }; } - layout(dimension: Dimension | undefined): void { - this.dimension = dimension; + layout(dimensions: ITitleControlDimensions): Dimension { - const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; - if (!activeTabAndIndex || !this.dimension) { - return; - } + // Remember dimensions that we get + Object.assign(this.dimensions, dimensions); // The layout of tabs can be an expensive operation because we access DOM properties // that can result in the browser doing a full page layout to validate them. To buffer // this a little bit we try at least to schedule this work on the next animation frame. if (!this.layoutScheduled.value) { this.layoutScheduled.value = scheduleAtNextAnimationFrame(() => { - const dimension = assertIsDefined(this.dimension); - this.doLayout(dimension); + this.doLayout(this.dimensions); this.layoutScheduled.clear(); }); } + + // First time layout: compute the dimensions and store it + if (!this.dimensions.used) { + this.dimensions.used = new Dimension(dimensions.container.width, this.getDimensions().height); + } + + return this.dimensions.used; } - private doLayout(dimension: Dimension): void { + private doLayout(dimensions: ITitleControlDimensions): void { + + // Only layout if we have valid tab index and dimensions const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; - if (!activeTabAndIndex) { - return; // nothing to do if not editor opened + if (activeTabAndIndex && dimensions.container !== Dimension.None && dimensions.available !== Dimension.None) { + + // Breadcrumbs + this.doLayoutBreadcrumbs(dimensions); + + // Tabs + const [activeTab, activeIndex] = activeTabAndIndex; + this.doLayoutTabs(activeTab, activeIndex, dimensions); } - // Breadcrumbs - this.doLayoutBreadcrumbs(dimension); + // Remember the dimensions used in the control so that we can + // return it fast from the `layout` call without having to + // compute it over and over again + const oldDimension = this.dimensions.used; + const newDimension = this.dimensions.used = new Dimension(dimensions.container.width, this.getDimensions().height); - // Tabs - const [activeTab, activeIndex] = activeTabAndIndex; - this.doLayoutTabs(activeTab, activeIndex); + // In case the height of the title control changed from before + // (currently only possible if wrapping changed on/off), we need + // to signal this to the outside via a `relayout` call so that + // e.g. the editor control can be adjusted accordingly. + if (oldDimension && oldDimension.height !== newDimension.height) { + this.group.relayout(); + } } - private doLayoutBreadcrumbs(dimension: Dimension): void { + private doLayoutBreadcrumbs(dimensions: ITitleControlDimensions): void { if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { - const tabsScrollbar = assertIsDefined(this.tabsScrollbar); - - this.breadcrumbsControl.layout(new Dimension(dimension.width, BreadcrumbsControl.HEIGHT)); - tabsScrollbar.getDomNode().style.height = `${dimension.height - BreadcrumbsControl.HEIGHT}px`; + this.breadcrumbsControl.layout(new Dimension(dimensions.container.width, BreadcrumbsControl.HEIGHT)); } } - private doLayoutTabs(activeTab: HTMLElement, activeIndex: number): void { + private doLayoutTabs(activeTab: HTMLElement, activeIndex: number, dimensions: ITitleControlDimensions): void { + + // Always first layout tabs with wrapping support even if wrapping + // is disabled. The result indicates if tabs wrap and if not, we + // need to proceed with the layout without wrapping because even + // if wrapping is enabled in settings, there are cases where + // wrapping is disabled (e.g. due to space constraints) + const tabsWrapMultiLine = this.doLayoutTabsWrapping(dimensions); + if (!tabsWrapMultiLine) { + this.doLayoutTabsNonWrapping(activeTab, activeIndex); + } + } + + private doLayoutTabsWrapping(dimensions: ITitleControlDimensions): boolean { + const [tabsAndActionsContainer, tabsContainer, editorToolbarContainer, tabsScrollbar] = assertAllDefined(this.tabsAndActionsContainer, this.tabsContainer, this.editorToolbarContainer, this.tabsScrollbar); + + // Handle wrapping tabs according to setting: + // - enabled: only add class if tabs wrap and don't exceed available dimensions + // - disabled: remove class and margin-right variable + + const didTabsWrapMultiLine = tabsAndActionsContainer.classList.contains('wrapping'); + let tabsWrapMultiLine = didTabsWrapMultiLine; + + function updateTabsWrapping(enabled: boolean): void { + tabsWrapMultiLine = enabled; + + // Toggle the `wrapped` class to enable wrapping + tabsAndActionsContainer.classList.toggle('wrapping', tabsWrapMultiLine); + + // Update `last-tab-margin-right` CSS variable to account for the absolute + // positioned editor actions container when tabs wrap. The margin needs to + // be the width of the editor actions container to avoid screen cheese. + tabsContainer.style.setProperty('--last-tab-margin-right', tabsWrapMultiLine ? `${editorToolbarContainer.offsetWidth}px` : '0'); + } + + // Setting enabled: selectively enable wrapping if possible + if (this.accessor.partOptions.wrapTabs) { + const visibleTabsWidth = tabsContainer.offsetWidth; + const allTabsWidth = tabsContainer.scrollWidth; + const lastTabFitsWrapped = () => { + const lastTab = this.getLastTab(); + if (!lastTab) { + return true; // no tab always fits + } + + return lastTab.offsetWidth <= (dimensions.available.width - editorToolbarContainer.offsetWidth); + }; + + // If tabs wrap or should start to wrap (when width exceeds visible width) + // we must trigger `updateWrapping` to set the `last-tab-margin-right` + // accordingly based on the number of actions. The margin is important to + // properly position the last tab apart from the actions + // + // We already check here if the last tab would fit when wrapped given the + // editor toolbar will also show right next to it. This ensures we are not + // enabling wrapping only to disable it again in the code below (this fixes + // flickering issue https://github.com/microsoft/vscode/issues/115050) + if (tabsWrapMultiLine || (allTabsWidth > visibleTabsWidth && lastTabFitsWrapped())) { + updateTabsWrapping(true); + } + + // Tabs wrap multiline: remove wrapping under certain size constraint conditions + if (tabsWrapMultiLine) { + if ( + (tabsContainer.offsetHeight > dimensions.available.height) || // if height exceeds available height + (allTabsWidth === visibleTabsWidth && tabsContainer.offsetHeight === TabsTitleControl.TAB_HEIGHT) || // if wrapping is not needed anymore + (!lastTabFitsWrapped()) // if last tab does not fit anymore + ) { + updateTabsWrapping(false); + } + } + } + + // Setting disabled: remove CSS traces only if tabs did wrap + else if (didTabsWrapMultiLine) { + updateTabsWrapping(false); + } + + // If we transitioned from non-wrapping to wrapping, we need + // to update the scrollbar to have an equal `width` and + // `scrollWidth`. Otherwise a scrollbar would appear which is + // never desired when wrapping. + if (tabsWrapMultiLine && !didTabsWrapMultiLine) { + const visibleTabsWidth = tabsContainer.offsetWidth; + tabsScrollbar.setScrollDimensions({ + width: visibleTabsWidth, + scrollWidth: visibleTabsWidth + }); + } + + return tabsWrapMultiLine; + } + + private doLayoutTabsNonWrapping(activeTab: HTMLElement, activeIndex: number): void { const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar); // @@ -1308,7 +1475,7 @@ export class TabsTitleControl extends TitleControl { // [-- Sticky Tabs Width --] // - const visibleTabsContainerWidth = tabsContainer.offsetWidth; + const visibleTabsWidth = tabsContainer.offsetWidth; const allTabsWidth = tabsContainer.scrollWidth; // Compute width of sticky tabs depending on pinned tab sizing @@ -1331,17 +1498,17 @@ export class TabsTitleControl extends TitleControl { } // Figure out if active tab is positioned static which has an - // impact on wether to reveal the tab or not later + // impact on whether to reveal the tab or not later let activeTabPositionStatic = this.accessor.partOptions.pinnedTabSizing !== 'normal' && this.group.isSticky(activeIndex); // Special case: we have sticky tabs but the available space for showing tabs // is little enough that we need to disable sticky tabs sticky positioning // so that tabs can be scrolled at naturally. - let availableTabsContainerWidth = visibleTabsContainerWidth - stickyTabsWidth; + let availableTabsContainerWidth = visibleTabsWidth - stickyTabsWidth; if (this.group.stickyCount > 0 && availableTabsContainerWidth < TabsTitleControl.TAB_WIDTH.fit) { tabsContainer.classList.add('disable-sticky-tabs'); - availableTabsContainerWidth = visibleTabsContainerWidth; + availableTabsContainerWidth = visibleTabsWidth; stickyTabsWidth = 0; activeTabPositionStatic = false; } else { @@ -1358,7 +1525,7 @@ export class TabsTitleControl extends TitleControl { // Update scrollbar tabsScrollbar.setScrollDimensions({ - width: visibleTabsContainerWidth, + width: visibleTabsWidth, scrollWidth: allTabsWidth }); @@ -1426,15 +1593,28 @@ export class TabsTitleControl extends TitleControl { private getTabAndIndex(editor: IEditorInput): [HTMLElement, number /* index */] | undefined { const editorIndex = this.group.getIndexOfEditor(editor); - if (editorIndex >= 0) { - const tabsContainer = assertIsDefined(this.tabsContainer); - - return [tabsContainer.children[editorIndex] as HTMLElement, editorIndex]; + const tab = this.getTabAtIndex(editorIndex); + if (tab) { + return [tab, editorIndex]; } return undefined; } + private getTabAtIndex(editorIndex: number): HTMLElement | undefined { + if (editorIndex >= 0) { + const tabsContainer = assertIsDefined(this.tabsContainer); + + return tabsContainer.children[editorIndex] as HTMLElement | undefined; + } + + return undefined; + } + + private getLastTab(): HTMLElement | undefined { + return this.getTabAtIndex(this.group.count - 1); + } + private blockRevealActiveTabOnce(): void { // When closing tabs through the tab close button or gesture, the user @@ -1527,16 +1707,28 @@ export class TabsTitleControl extends TitleControl { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { // Add border between tabs and breadcrumbs in high contrast mode. if (theme.type === ColorScheme.HIGH_CONTRAST) { const borderColor = (theme.getColor(TAB_BORDER) || theme.getColor(contrastBorder)); + if (borderColor) { + collector.addRule(` + .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container { + border-bottom: 1px solid ${borderColor}; + } + `); + } + } + + // Add bottom border to tabs when wrapping + const borderColor = theme.getColor(TAB_BORDER); + if (borderColor) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container { - border-bottom: 1px solid ${borderColor}; - } - `); + .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab { + border-bottom: 1px solid ${borderColor}; + } + `); } // Styling with Outline color (e.g. high contrast theme) diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 4557a2c48..7ae3677e1 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import * as objects from 'vs/base/common/objects'; -import { isFunction, isObject, isArray, assertIsDefined } from 'vs/base/common/types'; +import { isFunction, isObject, isArray, assertIsDefined, withUndefinedAsNull } from 'vs/base/common/types'; import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditorOptions, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; @@ -103,7 +103,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan } createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IDiffEditor { - return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration); + return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration, {}); } async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -132,8 +132,8 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan // Set Editor Model const diffEditor = assertIsDefined(this.getControl()); - const resolvedDiffEditorModel = resolvedModel; - diffEditor.setModel(resolvedDiffEditorModel.textDiffEditorModel); + const resolvedDiffEditorModel = resolvedModel as TextDiffEditorModel; + diffEditor.setModel(withUndefinedAsNull(resolvedDiffEditorModel.textDiffEditorModel)); // Apply Options from TextOptions let optionsGotApplied = false; diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index 9cae4d214..7ec16141d 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -233,7 +233,6 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa return undefined; } - protected saveTextEditorViewState(resource: URI, cleanUpOnDispose?: IEditorInput): void { const editorViewState = this.retrieveTextEditorViewState(resource); if (!editorViewState || !this.group) { diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 8358dfcba..6b7e1e5e9 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -9,14 +9,13 @@ import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ActionsOrientation, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, IActionViewItem } from 'vs/base/common/actions'; -import * as arrays from 'vs/base/common/arrays'; +import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, IActionViewItem } from 'vs/base/common/actions'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { localize } from 'vs/nls'; -import { createAndFillInActionBarActions, createAndFillInContextMenuActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { ExecuteCommandAction, IMenu, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { createActionViewItem, createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -26,7 +25,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; -import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceDataTransfers, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; @@ -34,7 +33,6 @@ import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/bro import { IEditorGroupsAccessor, IEditorGroupTitleDimensions, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { EditorCommandsContextActionRunner, IEditorCommandsContext, IEditorInput, EditorResourceAccessor, IEditorPartOptions, SideBySideEditor, ActiveEditorPinnedContext, ActiveEditorStickyContext } from 'vs/workbench/common/editor'; import { ResourceContextKey } from 'vs/workbench/common/resources'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { IFileService } from 'vs/platform/files/common/files'; import { withNullAsUndefined, withUndefinedAsNull, assertIsDefined } from 'vs/base/common/types'; @@ -46,6 +44,20 @@ export interface IToolbarActions { secondary: IAction[]; } +export interface ITitleControlDimensions { + + /** + * The size of the parent container the title control is layed out in. + */ + container: Dimension; + + /** + * The maximum size the title control is allowed to consume based on + * other controls that are positioned inside the container. + */ + available: Dimension; +} + export abstract class TitleControl extends Themable { protected readonly groupTransfer = LocalSelectionTransfer.getInstance(); @@ -53,9 +65,6 @@ export abstract class TitleControl extends Themable { protected breadcrumbsControl: BreadcrumbsControl | undefined = undefined; - private currentPrimaryEditorActionIds: string[] = []; - private currentSecondaryEditorActionIds: string[] = []; - private editorActionsToolbar: ToolBar | undefined; private resourceContext: ResourceContextKey; @@ -79,7 +88,6 @@ export abstract class TitleControl extends Themable { @IMenuService private readonly menuService: IMenuService, @IQuickInputService protected quickInputService: IQuickInputService, @IThemeService themeService: IThemeService, - @IExtensionService private readonly extensionService: IExtensionService, @IConfigurationService protected configurationService: IConfigurationService, @IFileService private readonly fileService: IFileService ) { @@ -92,13 +100,6 @@ export abstract class TitleControl extends Themable { this.contextMenu = this._register(this.menuService.createMenu(MenuId.EditorTitleContext, this.contextKeyService)); this.create(parent); - this.registerListeners(); - } - - protected registerListeners(): void { - - // Update actions toolbar when extension register that may contribute them - this._register(this.extensionService.onDidRegisterExtensions(() => this.updateEditorActionsToolbar())); } protected abstract create(parent: HTMLElement): void; @@ -147,7 +148,7 @@ export abstract class TitleControl extends Themable { this.editorActionsToolbar.context = context; // Action Run Handling - this._register(this.editorActionsToolbar.actionRunner.onDidRun((e: IRunEvent) => { + this._register(this.editorActionsToolbar.actionRunner.onDidRun(e => { // Notify for Error this.notificationService.error(e.error); @@ -172,35 +173,14 @@ export abstract class TitleControl extends Themable { } // Check extensions - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - - return undefined; + return createActionViewItem(this.instantiationService, action); } protected updateEditorActionsToolbar(): void { - - // Update Editor Actions Toolbar const { primaryEditorActions, secondaryEditorActions } = this.prepareEditorActions(this.getEditorActions()); - // Only update if something actually has changed - const primaryEditorActionIds = primaryEditorActions.map(a => a.id); - const secondaryEditorActionIds = secondaryEditorActions.map(a => a.id); - if ( - !arrays.equals(primaryEditorActionIds, this.currentPrimaryEditorActionIds) || - !arrays.equals(secondaryEditorActionIds, this.currentSecondaryEditorActionIds) || - primaryEditorActions.some(action => action instanceof ExecuteCommandAction) || // execute command actions can have the same ID but different arguments - secondaryEditorActions.some(action => action instanceof ExecuteCommandAction) // see also https://github.com/microsoft/vscode/issues/16298 - ) { - const editorActionsToolbar = assertIsDefined(this.editorActionsToolbar); - editorActionsToolbar.setActions(primaryEditorActions, secondaryEditorActions); - - this.currentPrimaryEditorActionIds = primaryEditorActionIds; - this.currentSecondaryEditorActionIds = secondaryEditorActionIds; - } + const editorActionsToolbar = assertIsDefined(this.editorActionsToolbar); + editorActionsToolbar.setActions(primaryEditorActions, secondaryEditorActions); } protected prepareEditorActions(editorActions: IToolbarActions): { primaryEditorActions: IAction[]; secondaryEditorActions: IAction[]; } { @@ -251,18 +231,13 @@ export abstract class TitleControl extends Themable { } protected clearEditorActionsToolbar(): void { - if (this.editorActionsToolbar) { - this.editorActionsToolbar.setActions([], []); - } - - this.currentPrimaryEditorActionIds = []; - this.currentSecondaryEditorActionIds = []; + this.editorActionsToolbar?.setActions([], []); } protected enableGroupDragging(element: HTMLElement): void { // Drag start - this._register(addDisposableListener(element, EventType.DRAG_START, (e: DragEvent) => { + this._register(addDisposableListener(element, EventType.DRAG_START, e => { if (e.target !== element) { return; // only if originating from tabs container } @@ -354,7 +329,7 @@ export abstract class TitleControl extends Themable { getAnchor: () => anchor, getActions: () => actions, getActionsContext: () => ({ groupId: this.group.id, editorIndex: this.group.getIndexOfEditor(editor) }), - getKeyBinding: (action) => this.getKeybinding(action), + getKeyBinding: action => this.getKeybinding(action), onHide: () => { // restore previous contexts @@ -407,7 +382,7 @@ export abstract class TitleControl extends Themable { abstract updateStyles(): void; - abstract layout(dimension: Dimension): void; + abstract layout(dimensions: ITitleControlDimensions): Dimension; abstract getDimensions(): IEditorGroupTitleDimensions; @@ -419,7 +394,7 @@ export abstract class TitleControl extends Themable { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { // Drag Feedback const dragImageBackground = theme.getColor(listActiveSelectionBackground); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index d4074bbeb..8113a026b 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/notificationsCenter'; import 'vs/css!./media/notificationsActions'; import { NOTIFICATIONS_BORDER, NOTIFICATIONS_CENTER_HEADER_FOREGROUND, NOTIFICATIONS_CENTER_HEADER_BACKGROUND, NOTIFICATIONS_CENTER_BORDER } from 'vs/workbench/common/theme'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector, Themable } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; import { INotificationsModel, INotificationChangeEvent, NotificationChangeType, NotificationViewItemContentChangeKind } from 'vs/workbench/common/notifications'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { Emitter } from 'vs/base/common/event'; @@ -313,7 +313,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { const notificationBorderColor = theme.getColor(NOTIFICATIONS_BORDER); if (notificationBorderColor) { collector.addRule(`.monaco-workbench > .notifications-center .notifications-list-container .monaco-list-row[data-last-element="false"] > .notification-list-item { border-bottom: 1px solid ${notificationBorderColor}; }`); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsList.ts b/src/vs/workbench/browser/parts/notifications/notificationsList.ts index 7cbfb84f2..b1e8112ea 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsList.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsList.ts @@ -10,7 +10,7 @@ import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IListOptions } from 'vs/base/browser/ui/list/listWidget'; import { NOTIFICATIONS_LINKS, NOTIFICATIONS_BACKGROUND, NOTIFICATIONS_FOREGROUND, NOTIFICATIONS_ERROR_ICON_FOREGROUND, NOTIFICATIONS_WARNING_ICON_FOREGROUND, NOTIFICATIONS_INFO_ICON_FOREGROUND } from 'vs/workbench/common/theme'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector, Themable } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; import { contrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { INotificationViewItem } from 'vs/workbench/common/notifications'; import { NotificationsListDelegate, NotificationRenderer } from 'vs/workbench/browser/parts/notifications/notificationsViewer'; @@ -278,7 +278,7 @@ export class NotificationsList extends Themable { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { const linkColor = theme.getColor(NOTIFICATIONS_LINKS); if (linkColor) { collector.addRule(`.monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-message a { color: ${linkColor}; }`); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 51272ab8a..2c8b0f465 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -470,7 +470,9 @@ export class NotificationTemplateRenderer extends Disposable { : buttonToolbar.addButton(buttonOptions)); button.label = action.label; this.inputDisposables.add(button.onDidClick(e => { - EventHelper.stop(e, true); + if (e) { + EventHelper.stop(e, true); + } actionRunner.run(action); })); diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 74619ecec..372e78f8a 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -5,45 +5,26 @@ import 'vs/css!./media/panelpart'; import * as nls from 'vs/nls'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Action } from 'vs/base/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as WorkbenchExtensions, CATEGORIES } from 'vs/workbench/common/actions'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IWorkbenchLayoutService, Parts, Position, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { ActivityAction, ToggleCompositePinnedAction, ICompositeBar } from 'vs/workbench/browser/parts/compositeBarActions'; import { IActivity } from 'vs/workbench/common/activity'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ActivePanelContext, PanelPositionContext } from 'vs/workbench/common/panel'; -import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; +import { ActivePanelContext, PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from 'vs/workbench/common/panel'; +import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ViewContainerLocationToString, ViewContainerLocation } from 'vs/workbench/common/views'; const maximizeIcon = registerIcon('panel-maximize', Codicon.chevronUp, nls.localize('maximizeIcon', 'Icon to maximize a panel.')); const restoreIcon = registerIcon('panel-restore', Codicon.chevronDown, nls.localize('restoreIcon', 'Icon to restore a panel.')); const closeIcon = registerIcon('panel-close', Codicon.close, nls.localize('closeIcon', 'Icon to close a panel.')); -export class ClosePanelAction extends Action { - - static readonly ID = 'workbench.action.closePanel'; - static readonly LABEL = nls.localize('closePanel', "Close Panel"); - - constructor( - id: string, - name: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService - ) { - super(id, name, ThemeIcon.asClassName(closeIcon)); - } - - async run(): Promise { - this.layoutService.setPanelHidden(true); - } -} - export class TogglePanelAction extends Action { static readonly ID = 'workbench.action.togglePanel'; @@ -91,46 +72,6 @@ class FocusPanelAction extends Action { } } - -export class ToggleMaximizedPanelAction extends Action { - - static readonly ID = 'workbench.action.toggleMaximizedPanel'; - static readonly LABEL = nls.localize('toggleMaximizedPanel', "Toggle Maximized Panel"); - - private static readonly MAXIMIZE_LABEL = nls.localize('maximizePanel', "Maximize Panel Size"); - private static readonly RESTORE_LABEL = nls.localize('minimizePanel', "Restore Panel Size"); - - private readonly toDispose = this._register(new DisposableStore()); - - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IEditorGroupsService editorGroupsService: IEditorGroupsService - ) { - super(id, label, layoutService.isPanelMaximized() ? ThemeIcon.asClassName(restoreIcon) : ThemeIcon.asClassName(maximizeIcon)); - - this.toDispose.add(editorGroupsService.onDidLayout(() => { - const maximized = this.layoutService.isPanelMaximized(); - this.class = maximized ? ThemeIcon.asClassName(restoreIcon) : ThemeIcon.asClassName(maximizeIcon); - this.label = maximized ? ToggleMaximizedPanelAction.RESTORE_LABEL : ToggleMaximizedPanelAction.MAXIMIZE_LABEL; - })); - } - - async run(): Promise { - if (!this.layoutService.isVisible(Parts.PANEL_PART)) { - this.layoutService.setPanelHidden(false); - // If the panel is not already maximized, maximize it - if (!this.layoutService.isPanelMaximized()) { - this.layoutService.toggleMaximizedPanel(); - } - } - else { - this.layoutService.toggleMaximizedPanel(); - } - } -} - const PositionPanelActionId = { LEFT: 'workbench.action.positionPanelLeft', RIGHT: 'workbench.action.positionPanelRight', @@ -218,7 +159,6 @@ export class PlaceHolderToggleCompositePinnedAction extends ToggleCompositePinne } } - export class SwitchPanelViewAction extends Action { constructor( @@ -287,35 +227,117 @@ export class NextPanelViewAction extends SwitchPanelViewAction { const actionRegistry = Registry.as(WorkbenchExtensions.WorkbenchActions); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(TogglePanelAction, { primary: KeyMod.CtrlCmd | KeyCode.KEY_J }), 'View: Toggle Panel', CATEGORIES.View.value); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(FocusPanelAction), 'View: Focus into Panel', CATEGORIES.View.value); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleMaximizedPanelAction), 'View: Toggle Maximized Panel', CATEGORIES.View.value); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ClosePanelAction), 'View: Close Panel', CATEGORIES.View.value); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(PreviousPanelViewAction), 'View: Previous Panel View', CATEGORIES.View.value); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(NextPanelViewAction), 'View: Next Panel View', CATEGORIES.View.value); -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: TogglePanelAction.ID, - title: nls.localize({ key: 'miShowPanel', comment: ['&& denotes a mnemonic'] }, "Show &&Panel"), - toggled: ActivePanelContext - }, - order: 5 +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.toggleMaximizedPanel', + title: { value: nls.localize('toggleMaximizedPanel', "Toggle Maximized Panel"), original: 'Toggle Maximized Panel' }, + tooltip: nls.localize('maximizePanel', "Maximize Panel Size"), + category: CATEGORIES.View, + f1: true, + icon: maximizeIcon, + toggled: { condition: PanelMaximizedContext, icon: restoreIcon, tooltip: nls.localize('minimizePanel', "Restore Panel Size") }, + menu: [{ + id: MenuId.PanelTitle, + group: 'navigation', + order: 1 + }] + }); + } + run(accessor: ServicesAccessor) { + const layoutService = accessor.get(IWorkbenchLayoutService); + if (!layoutService.isVisible(Parts.PANEL_PART)) { + layoutService.setPanelHidden(false); + // If the panel is not already maximized, maximize it + if (!layoutService.isPanelMaximized()) { + layoutService.toggleMaximizedPanel(); + } + } + else { + layoutService.toggleMaximizedPanel(); + } + } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.closePanel', + title: { value: nls.localize('closePanel', "Close Panel"), original: 'Close Panel' }, + category: CATEGORIES.View, + icon: closeIcon, + menu: [{ + id: MenuId.CommandPalette, + when: PanelVisibleContext, + }, { + id: MenuId.PanelTitle, + group: 'navigation', + order: 2 + }] + }); + } + run(accessor: ServicesAccessor) { + accessor.get(IWorkbenchLayoutService).setPanelHidden(true); + } +}); + +MenuRegistry.appendMenuItems([ + { + id: MenuId.MenubarAppearanceMenu, + item: { + group: '2_workbench_layout', + command: { + id: TogglePanelAction.ID, + title: nls.localize({ key: 'miShowPanel', comment: ['&& denotes a mnemonic'] }, "Show &&Panel"), + toggled: ActivePanelContext + }, + order: 5 + } + }, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: TogglePanelAction.ID, + title: { value: nls.localize('hidePanel', "Hide Panel"), original: 'Hide Panel' }, + }, + when: ContextKeyExpr.and(PanelVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Panel))), + order: 2 + } + } +]); + function registerPositionPanelActionById(config: PanelActionConfig) { const { id, label, alias, when } = config; // register the workbench action actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(SetPanelPositionAction, id, label), alias, CATEGORIES.View.value, when); // register as a menu item - MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '3_workbench_layout_move', - command: { - id, - title: label - }, - when, - order: 5 - }); + MenuRegistry.appendMenuItems([{ + id: MenuId.MenubarAppearanceMenu, + item: { + group: '3_workbench_layout_move', + command: { + id, + title: label + }, + when, + order: 5 + } + }, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: id, + title: label, + }, + when: ContextKeyExpr.and(when, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Panel))), + order: 1 + } + }]); } // register each position panel action diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 332a9ab4c..b4727ebc0 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/panelpart'; -import { IAction, Action } from 'vs/base/common/actions'; +import { localize } from 'vs/nls'; +import { IAction, Separator, toAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -18,8 +19,8 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ClosePanelAction, PanelActivityAction, ToggleMaximizedPanelAction, TogglePanelAction, PlaceHolderPanelActivityAction, PlaceHolderToggleCompositePinnedAction, PositionPanelActionConfigs, SetPanelPositionAction } from 'vs/workbench/browser/parts/panel/panelActions'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { PanelActivityAction, TogglePanelAction, PlaceHolderPanelActivityAction, PlaceHolderToggleCompositePinnedAction, PositionPanelActionConfigs, SetPanelPositionAction } from 'vs/workbench/browser/parts/panel/panelActions'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_INPUT_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, PANEL_DRAG_AND_DROP_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, focusBorder, contrastBorder, editorBackground, badgeBackground, badgeForeground } from 'vs/platform/theme/common/colorRegistry'; import { CompositeBar, ICompositeBarItem, CompositeDragAndDrop } from 'vs/workbench/browser/parts/compositeBar'; @@ -27,15 +28,12 @@ import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/composit import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { Dimension, trackFocus, EventHelper } from 'vs/base/browser/dom'; -import { localize } from 'vs/nls'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { isUndefinedOrNull, assertIsDefined } from 'vs/base/common/types'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ViewContainer, IViewDescriptorService, IViewContainerModel, ViewContainerLocation } from 'vs/workbench/common/views'; -import { MenuId } from 'vs/platform/actions/common/actions'; -import { ViewMenuActions, ViewContainerMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { Before2D, CompositeDragAndDropObserver, ICompositeDragAndDrop, toggleDropEffect } from 'vs/workbench/browser/dnd'; import { IActivity } from 'vs/workbench/common/activity'; @@ -46,7 +44,7 @@ interface ICachedPanel { pinned: boolean; order?: number; visible: boolean; - views?: { when?: string }[]; + views?: { when?: string; }[]; } interface IPlaceholderViewContainer { @@ -148,24 +146,27 @@ export class PanelPart extends CompositePart implements IPanelService { this.compositeBar = this._register(this.instantiationService.createInstance(CompositeBar, this.getCachedPanels(), { icon: false, orientation: ActionsOrientation.HORIZONTAL, - openComposite: (compositeId: string) => this.openPanel(compositeId, true).then(panel => panel || null), - getActivityAction: (compositeId: string) => this.getCompositeActions(compositeId).activityAction, - getCompositePinnedAction: (compositeId: string) => this.getCompositeActions(compositeId).pinnedAction, - getOnCompositeClickAction: (compositeId: string) => this.instantiationService.createInstance(PanelActivityAction, assertIsDefined(this.getPanel(compositeId))), - getContextMenuActions: () => [ - ...PositionPanelActionConfigs - // show the contextual menu item if it is not in that position - .filter(({ when }) => contextKeyService.contextMatchesRules(when)) - .map(({ id, label }) => this.instantiationService.createInstance(SetPanelPositionAction, id, label)), - this.instantiationService.createInstance(TogglePanelAction, TogglePanelAction.ID, localize('hidePanel', "Hide Panel")) - ] as Action[], - getContextMenuActionsForComposite: (compositeId: string) => this.getContextMenuActionsForComposite(compositeId) as Action[], + openComposite: compositeId => this.openPanel(compositeId, true).then(panel => panel || null), + getActivityAction: compositeId => this.getCompositeActions(compositeId).activityAction, + getCompositePinnedAction: compositeId => this.getCompositeActions(compositeId).pinnedAction, + getOnCompositeClickAction: compositeId => this.instantiationService.createInstance(PanelActivityAction, assertIsDefined(this.getPanel(compositeId))), + fillExtraContextMenuActions: actions => { + actions.push(...[ + new Separator(), + ...PositionPanelActionConfigs + // show the contextual menu item if it is not in that position + .filter(({ when }) => contextKeyService.contextMatchesRules(when)) + .map(({ id, label }) => this.instantiationService.createInstance(SetPanelPositionAction, id, label)), + this.instantiationService.createInstance(TogglePanelAction, TogglePanelAction.ID, localize('hidePanel', "Hide Panel")) + ]); + }, + getContextMenuActionsForComposite: compositeId => this.getContextMenuActionsForComposite(compositeId), getDefaultCompositeId: () => this.panelRegistry.getDefaultPanelId(), hidePart: () => this.layoutService.setPanelHidden(true), dndHandler: this.dndHandler, compositeSize: 0, overflowActionSize: 44, - colors: (theme: IColorTheme) => ({ + colors: theme => ({ activeBackgroundColor: theme.getColor(PANEL_BACKGROUND), // Background color for overflow action inactiveBackgroundColor: theme.getColor(PANEL_BACKGROUND), // Background color for overflow action activeBorderBottomColor: theme.getColor(PANEL_ACTIVE_TITLE_BORDER), @@ -184,20 +185,21 @@ export class PanelPart extends CompositePart implements IPanelService { this.onDidRegisterPanels([...this.getPanels()]); } - private getContextMenuActionsForComposite(compositeId: string): readonly IAction[] { + private getContextMenuActionsForComposite(compositeId: string): IAction[] { const result: IAction[] = []; - const container = this.getViewContainer(compositeId); - if (container) { - const viewContainerModel = this.viewDescriptorService.getViewContainerModel(container); + const viewContainer = this.viewDescriptorService.getViewContainerById(compositeId)!; + const defaultLocation = this.viewDescriptorService.getDefaultViewContainerLocation(viewContainer)!; + if (defaultLocation !== this.viewDescriptorService.getViewContainerLocation(viewContainer)) { + result.push(toAction({ id: 'resetLocationAction', label: localize('resetLocation', "Reset Location"), run: () => this.viewDescriptorService.moveViewContainerToLocation(viewContainer, defaultLocation) })); + } else { + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); if (viewContainerModel.allViewDescriptors.length === 1) { - const viewMenuActions = this.instantiationService.createInstance(ViewMenuActions, viewContainerModel.allViewDescriptors[0].id, MenuId.ViewTitle, MenuId.ViewTitleContext); - result.push(...viewMenuActions.getContextMenuActions()); - viewMenuActions.dispose(); + const viewToReset = viewContainerModel.allViewDescriptors[0]; + const defaultContainer = this.viewDescriptorService.getDefaultContainerById(viewToReset.id)!; + if (defaultContainer !== viewContainer) { + result.push(toAction({ id: 'resetLocationAction', label: localize('resetLocation', "Reset Location"), run: () => this.viewDescriptorService.moveViewsToContainer([viewToReset], defaultContainer) })); + } } - - const viewContainerMenuActions = this.instantiationService.createInstance(ViewContainerMenuActions, container.id, MenuId.ViewContainerTitleContext); - result.push(...viewContainerMenuActions.getContextMenuActions()); - viewContainerMenuActions.dispose(); } return result; } @@ -534,13 +536,6 @@ export class PanelPart extends CompositePart implements IPanelService { .sort((p1, p2) => pinnedCompositeIds.indexOf(p1.id) - pinnedCompositeIds.indexOf(p2.id)); } - protected getActions(): ReadonlyArray { - return [ - this.instantiationService.createInstance(ToggleMaximizedPanelAction, ToggleMaximizedPanelAction.ID, ToggleMaximizedPanelAction.LABEL), - this.instantiationService.createInstance(ClosePanelAction, ClosePanelAction.ID, ClosePanelAction.LABEL) - ]; - } - getActivePanel(): IPanel | undefined { return this.getActiveComposite(); } @@ -803,7 +798,7 @@ export class PanelPart extends CompositePart implements IPanelService { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { // Panel Background: since panels can host editors, we apply a background rule if the panel background // color is different from the editor background color. This is a bit of a hack though. The better way diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index c5953e535..c581f528b 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -3,11 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* Removed to allow progress bar positioning to escape */ -/* .monaco-workbench .sidebar > .content { - overflow: hidden; -} */ - .monaco-workbench.nosidebar > .part.sidebar { display: none !important; visibility: hidden !important; diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index f6504de93..671a05822 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -6,7 +6,6 @@ import 'vs/css!./media/sidebarpart'; import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Action } from 'vs/base/common/actions'; import { CompositePart } from 'vs/workbench/browser/parts/compositePart'; import { Viewlet, ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; @@ -288,8 +287,8 @@ export class SidebarPart extends CompositePart implements IViewletServi const anchor: { x: number, y: number } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, - getActions: () => contextMenuActions, - getActionViewItem: action => this.actionViewItemProvider(action as Action), + getActions: () => contextMenuActions.slice(), + getActionViewItem: action => this.actionViewItemProvider(action), actionRunner: activeViewlet.getActionRunner() }); } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 759039699..aa9d9f05f 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/statusbarpart'; import * as nls from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { dispose, IDisposable, Disposable, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; +import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Part } from 'vs/workbench/browser/part'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -15,13 +15,12 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor } from 'vs/workbench/services/statusbar/common/statusbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Action, IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, Separator } from 'vs/base/common/actions'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector, ThemeColor } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, ThemeColor } from 'vs/platform/theme/common/themeService'; import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER } from 'vs/workbench/common/theme'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { isThemeColor } from 'vs/editor/common/editorCommon'; -import { Color } from 'vs/base/common/color'; -import { EventHelper, createStyleSheet, addDisposableListener, EventType, hide, show, isAncestor, appendChildren } from 'vs/base/browser/dom'; +import { EventHelper, createStyleSheet, addDisposableListener, EventType, hide, show, isAncestor, append } from 'vs/base/browser/dom'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, IStorageValueChangeEvent, StorageTarget } from 'vs/platform/storage/common/storage'; import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -38,7 +37,8 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { renderCodicon, renderCodicons } from 'vs/base/browser/codicons'; +import { renderIcon, renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { syncing } from 'vs/platform/theme/common/iconRegistry'; interface IPendingStatusbarEntry { id: string; @@ -610,7 +610,7 @@ export class StatusbarPart extends Part implements IStatusbarService { } private getContextMenuActions(event: StandardMouseEvent): IAction[] { - const actions: Action[] = []; + const actions: IAction[] = []; // Provide an action to hide the status bar at last actions.push(this.instantiationService.createInstance(ToggleStatusbarVisibilityAction, ToggleStatusbarVisibilityAction.ID, nls.localize('hideStatusBar', "Hide Status Bar"))); @@ -704,9 +704,9 @@ export class StatusbarPart extends Part implements IStatusbarService { } } -class StatusBarCodiconLabel extends CodiconLabel { +class StatusBarCodiconLabel extends SimpleIconLabel { - private readonly progressCodicon = renderCodicon('sync', 'spin'); + private readonly progressCodicon = renderIcon(syncing); private currentText = ''; private currentShowProgress = false; @@ -749,7 +749,7 @@ class StatusBarCodiconLabel extends CodiconLabel { } // Append new elements - appendChildren(this.container, ...renderCodicons(textContent)); + append(this.container, ...renderLabelWithIcons(textContent)); } // No Progress: no special handling @@ -868,12 +868,8 @@ class StatusbarEntryItem extends Disposable { // Update: Background if (!this.entry || entry.backgroundColor !== this.entry.backgroundColor) { - if (entry.backgroundColor) { - this.applyColor(this.container, entry.backgroundColor, true); - this.container.classList.add('has-background-color'); - } else { - this.container.classList.remove('has-background-color'); - } + this.container.classList.toggle('has-background-color', !!entry.backgroundColor); + this.applyColor(this.container, entry.backgroundColor, true); } // Remember for next round @@ -893,7 +889,7 @@ class StatusbarEntryItem extends Disposable { } private applyColor(container: HTMLElement, color: string | ThemeColor | undefined, isBackground?: boolean): void { - let colorResult: string | null = null; + let colorResult: string | undefined = undefined; if (isBackground) { this.backgroundListener.clear(); @@ -903,15 +899,15 @@ class StatusbarEntryItem extends Disposable { if (color) { if (isThemeColor(color)) { - colorResult = (this.themeService.getColorTheme().getColor(color.id) || Color.transparent).toString(); + colorResult = this.themeService.getColorTheme().getColor(color.id)?.toString(); const listener = this.themeService.onDidColorThemeChange(theme => { - const colorValue = (theme.getColor(color.id) || Color.transparent).toString(); + const colorValue = theme.getColor(color.id)?.toString(); if (isBackground) { - container.style.backgroundColor = colorValue; + container.style.backgroundColor = colorValue ?? ''; } else { - container.style.color = colorValue; + container.style.color = colorValue ?? ''; } }); @@ -926,9 +922,9 @@ class StatusbarEntryItem extends Disposable { } if (isBackground) { - container.style.backgroundColor = colorResult || ''; + container.style.backgroundColor = colorResult ?? ''; } else { - container.style.color = colorResult || ''; + container.style.color = colorResult ?? ''; } } @@ -942,7 +938,7 @@ class StatusbarEntryItem extends Disposable { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { if (theme.type !== ColorScheme.HIGH_CONTRAST) { const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); if (statusBarItemHoverBackground) { diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index a4c34e450..7ac774cb7 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -85,11 +85,34 @@ height: 100%; position: relative; z-index: 3000; + flex-shrink: 0; +} + +.monaco-workbench .part.titlebar > .window-appicon:not(.codicon) { background-image: url('../../../media/code-icon.svg'); background-repeat: no-repeat; background-position: center center; background-size: 16px; - flex-shrink: 0; +} + +.monaco-workbench .part.titlebar .window-appicon > .home-bar-icon-badge { + position: absolute; + right: 9px; + bottom: 6px; + width: 8px; + height: 8px; + z-index: 1; /* on top of home indicator */ + background-image: url('../../../media/code-icon.svg'); + background-repeat: no-repeat; + background-position: center center; + background-size: 8px; + pointer-events: none; + border-top: 1px solid transparent; + border-left: 1px solid transparent; +} + +.monaco-workbench .part.titlebar > .window-appicon.codicon { + line-height: 30px; } .monaco-workbench.fullscreen .part.titlebar > .window-appicon { diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 99fcf2b6e..886c2ff68 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { IMenuService, MenuId, IMenu, SubmenuItemAction, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; -import { registerThemingParticipant, IColorTheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService'; +import { IMenuService, MenuId, IMenu, SubmenuItemAction, registerAction2, Action2, MenuRegistry, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { registerThemingParticipant, IThemeService } from 'vs/platform/theme/common/themeService'; import { MenuBarVisibility, getTitleBarStyle, IWindowOpenable, getMenuBarVisibility } from 'vs/platform/windows/common/windows'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IAction, Action, SubmenuAction, Separator } from 'vs/base/common/actions'; import * as DOM from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -37,6 +37,7 @@ import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { KeyCode } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; +import { ICommandService } from 'vs/platform/commands/common/commands'; export abstract class MenubarControl extends Disposable { @@ -91,7 +92,8 @@ export abstract class MenubarControl extends Disposable { protected readonly preferencesService: IPreferencesService, protected readonly environmentService: IWorkbenchEnvironmentService, protected readonly accessibilityService: IAccessibilityService, - protected readonly hostService: IHostService + protected readonly hostService: IHostService, + protected readonly commandService: ICommandService ) { super(); @@ -308,23 +310,10 @@ export class CustomMenubarControl extends MenubarControl { @IAccessibilityService accessibilityService: IAccessibilityService, @IThemeService private readonly themeService: IThemeService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IHostService protected readonly hostService: IHostService + @IHostService protected readonly hostService: IHostService, + @ICommandService commandService: ICommandService ) { - super( - menuService, - workspacesService, - contextKeyService, - keybindingService, - configurationService, - labelService, - updateService, - storageService, - notificationService, - preferencesService, - environmentService, - accessibilityService, - hostService - ); + super(menuService, workspacesService, contextKeyService, keybindingService, configurationService, labelService, updateService, storageService, notificationService, preferencesService, environmentService, accessibilityService, hostService, commandService); this._onVisibilityChange = this._register(new Emitter()); this._onFocusStateChange = this._register(new Emitter()); @@ -337,7 +326,17 @@ export class CustomMenubarControl extends MenubarControl { this.registerActions(); - registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { + // Register web menu actions to the file menu when its in the title + this.getWebNavigationMenuItemActions().forEach(actionItem => { + this._register(MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + command: actionItem.item, + title: actionItem.item.title, + group: 'z_Web', + when: ContextKeyExpr.and(IsWebContext, ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'compact')) + })); + }); + + registerThemingParticipant((theme, collector) => { const menubarActiveWindowFgColor = theme.getColor(TITLE_BAR_ACTIVE_FOREGROUND); if (menubarActiveWindowFgColor) { collector.addRule(` @@ -356,7 +355,6 @@ export class CustomMenubarControl extends MenubarControl { color: ${activityBarInactiveFgColor}; } `); - } const activityBarFgColor = theme.getColor(ACTIVITY_BAR_FOREGROUND); @@ -383,7 +381,6 @@ export class CustomMenubarControl extends MenubarControl { `); } - const menubarSelectedFgColor = theme.getColor(MENUBAR_SELECTION_FOREGROUND); if (menubarSelectedFgColor) { collector.addRule(` @@ -608,6 +605,12 @@ export class CustomMenubarControl extends MenubarControl { for (let action of actions) { this.insertActionsBefore(action, target); + + // use mnemonicTitle whenever possible + const title = typeof action.item.title === 'string' + ? action.item.title + : action.item.title.mnemonicTitle ?? action.item.title.value; + if (action instanceof SubmenuItemAction) { let submenu = this.menus[action.item.submenu.id]; if (!submenu) { @@ -625,10 +628,12 @@ export class CustomMenubarControl extends MenubarControl { const submenuActions: SubmenuAction[] = []; updateActions(submenu, submenuActions, topLevelTitle); - target.push(new SubmenuAction(action.id, mnemonicMenuLabel(action.label), submenuActions)); + target.push(new SubmenuAction(action.id, mnemonicMenuLabel(title), submenuActions)); } else { - action.label = mnemonicMenuLabel(this.calculateActionLabel(action)); - target.push(action); + const newAction = new Action(action.id, mnemonicMenuLabel(title), action.class, action.enabled, () => this.commandService.executeCommand(action.id)); + newAction.tooltip = action.tooltip; + newAction.checked = action.checked; + target.push(newAction); } } @@ -667,6 +672,27 @@ export class CustomMenubarControl extends MenubarControl { } } + private getWebNavigationMenuItemActions(): MenuItemAction[] { + if (!isWeb) { + return []; // only for web + } + + const webNavigationActions = []; + const webNavigationMenu = this.menuService.createMenu(MenuId.MenubarHomeMenu, this.contextKeyService); + for (const groups of webNavigationMenu.getActions()) { + const [, actions] = groups; + for (const action of actions) { + if (action instanceof MenuItemAction) { + webNavigationActions.push(action); + } + } + } + + webNavigationMenu.dispose(); + + return webNavigationActions; + } + private getMenuBarOptions(): IMenuBarOptions { return { enableMnemonics: this.currentEnableMenuBarMnemonics, @@ -674,7 +700,35 @@ export class CustomMenubarControl extends MenubarControl { visibility: this.currentMenubarVisibility, getKeybinding: (action) => this.keybindingService.lookupKeybinding(action.id), alwaysOnMnemonics: this.alwaysOnMnemonics, - compactMode: this.currentCompactMenuMode + compactMode: this.currentCompactMenuMode, + getCompactMenuActions: () => { + if (!isWeb) { + return []; // only for web + } + + const webNavigationActions: IAction[] = []; + const href = this.environmentService.options?.homeIndicator?.href; + if (href) { + webNavigationActions.push(new Action('goHome', nls.localize('goHome', "Go Home"), undefined, true, + async (event?: MouseEvent) => { + if ((!isMacintosh && event?.ctrlKey) || (isMacintosh && event?.metaKey)) { + window.open(href, '_blank'); + } else { + window.location.href = href; + } + })); + } + + const otherActions = this.getWebNavigationMenuItemActions().map(action => { + const title = typeof action.item.title === 'string' + ? action.item.title + : action.item.title.mnemonicTitle ?? action.item.title.value; + return new Action(action.id, mnemonicMenuLabel(title), action.class, action.enabled, () => this.commandService.executeCommand(action.id)); + }); + + webNavigationActions.push(...otherActions); + return webNavigationActions; + } }; } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 4783cb522..52a1f2f45 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/titlebarpart'; +import { localize } from 'vs/nls'; import { dirname, basename } from 'vs/base/common/resources'; import { Part } from 'vs/workbench/browser/part'; import { ITitleService, ITitleProperties } from 'vs/workbench/services/title/common/titleService'; @@ -15,17 +16,16 @@ import { IAction } from 'vs/base/common/actions'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import * as nls from 'vs/nls'; import { EditorResourceAccessor, Verbosity, SideBySideEditor } from 'vs/workbench/common/editor'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_BACKGROUND, TITLE_BAR_BORDER, WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; import { isMacintosh, isWindows, isLinux, isWeb } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { Color } from 'vs/base/common/color'; import { trim } from 'vs/base/common/strings'; -import { EventType, EventHelper, Dimension, isAncestor, append, $, addDisposableListener, runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; +import { EventType, EventHelper, Dimension, isAncestor, append, $, addDisposableListener, runAtThisOrScheduleAtNextAnimationFrame, prepend } from 'vs/base/browser/dom'; import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { template } from 'vs/base/common/labels'; @@ -41,12 +41,13 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IProductService } from 'vs/platform/product/common/productService'; import { Schemas } from 'vs/base/common/network'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { Codicon, iconRegistry } from 'vs/base/common/codicons'; export class TitlebarPart extends Part implements ITitleService { - private static readonly NLS_UNSUPPORTED = nls.localize('patchedWindowTitle', "[Unsupported]"); - private static readonly NLS_USER_IS_ADMIN = isWindows ? nls.localize('userIsAdmin', "[Administrator]") : nls.localize('userIsSudo', "[Superuser]"); - private static readonly NLS_EXTENSION_HOST = nls.localize('devExtensionWindowTitlePrefix', "[Extension Development Host]"); + private static readonly NLS_UNSUPPORTED = localize('patchedWindowTitle', "[Unsupported]"); + private static readonly NLS_USER_IS_ADMIN = isWindows ? localize('userIsAdmin', "[Administrator]") : localize('userIsSudo', "[Superuser]"); + private static readonly NLS_EXTENSION_HOST = localize('devExtensionWindowTitlePrefix', "[Extension Development Host]"); private static readonly TITLE_DIRTY = '\u25cf '; //#region IView @@ -65,6 +66,8 @@ export class TitlebarPart extends Part implements ITitleService { protected title!: HTMLElement; protected customMenubar: CustomMenubarControl | undefined; + protected appIcon: HTMLElement | undefined; + private appIconBadge: HTMLElement | undefined; protected menubar?: HTMLElement; protected lastLayoutDimensions: Dimension | undefined; private titleBarStyle: 'native' | 'custom'; @@ -86,7 +89,7 @@ export class TitlebarPart extends Part implements ITitleService { @IEditorService private readonly editorService: IEditorService, @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @ILabelService private readonly labelService: ILabelService, @IStorageService storageService: IStorageService, @@ -341,6 +344,28 @@ export class TitlebarPart extends Part implements ITitleService { createContentArea(parent: HTMLElement): HTMLElement { this.element = parent; + // App Icon (Native Windows/Linux and Web) + if (!isMacintosh || isWeb) { + this.appIcon = prepend(this.element, $('a.window-appicon')); + + // Web-only home indicator and menu + if (isWeb) { + const homeIndicator = this.environmentService.options?.homeIndicator; + if (homeIndicator) { + let codicon = iconRegistry.get(homeIndicator.icon); + if (!codicon) { + codicon = Codicon.code; + } + + this.appIcon.setAttribute('href', homeIndicator.href); + this.appIcon.classList.add(...codicon.classNamesArray); + this.appIconBadge = document.createElement('div'); + this.appIconBadge.classList.add('home-bar-icon-badge'); + this.appIcon.appendChild(this.appIconBadge); + } + } + } + // Menubar: install a custom menu bar depending on configuration // and when not in activity bar if (this.titleBarStyle !== 'native' @@ -407,6 +432,11 @@ export class TitlebarPart extends Part implements ITitleService { return color.isOpaque() ? color : color.makeOpaque(WORKBENCH_BACKGROUND(theme)); }) || ''; this.element.style.backgroundColor = titleBackground; + + if (this.appIconBadge) { + this.appIconBadge.style.backgroundColor = titleBackground; + } + if (titleBackground && Color.fromHex(titleBackground).isLighter()) { this.element.classList.add('light'); } else { @@ -441,7 +471,7 @@ export class TitlebarPart extends Part implements ITitleService { protected adjustTitleMarginToCenter(): void { if (this.customMenubar && this.menubar) { - const leftMarker = this.menubar.clientWidth + 10; + const leftMarker = (this.appIcon ? this.appIcon.clientWidth : 0) + this.menubar.clientWidth + 10; const rightMarker = this.element.clientWidth - 10; // Not enough space to center the titlebar within window, @@ -497,7 +527,7 @@ export class TitlebarPart extends Part implements ITitleService { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { const titlebarActiveFg = theme.getColor(TITLE_BAR_ACTIVE_FOREGROUND); if (titlebarActiveFg) { collector.addRule(` diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index dbd8ffce1..73c7e5e45 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -8,19 +8,19 @@ import { toDisposable, IDisposable, Disposable, DisposableStore } from 'vs/base/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { MenuId, IMenuService, MenuItemAction, registerAction2, Action2, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { MenuId, IMenuService, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem } from 'vs/workbench/common/views'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IThemeService, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { Registry } from 'vs/platform/registry/common/platform'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Event, Emitter } from 'vs/base/common/event'; import { IAction, ActionRunner, IActionViewItemProvider } from 'vs/base/common/actions'; -import { MenuEntryActionViewItem, createAndFillInContextMenuActions, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createAndFillInContextMenuActions, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -52,6 +52,8 @@ import { IIconLabelMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabel import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { Codicon } from 'vs/base/common/codicons'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Command } from 'vs/editor/common/modes'; export class TreeViewPane extends ViewPane { @@ -453,15 +455,7 @@ export class TreeView extends Disposable implements ITreeView { } private createTree() { - const actionViewItemProvider = (action: IAction) => { - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - - return undefined; - }; + const actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService); const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.id }, () => task)); @@ -534,12 +528,13 @@ export class TreeView extends Disposable implements ITreeView { })); this.tree.setInput(this.root).then(() => this.updateContentAreas()); - this._register(this.tree.onDidOpen(e => { + this._register(this.tree.onDidOpen(async (e) => { if (!e.browserEvent) { return; } const selection = this.tree!.getSelection(); - const command = selection.length === 1 ? selection[0].command : undefined; + const command = await this.resolveCommand(selection.length === 1 ? selection[0] : undefined); + if (command) { let args = command.arguments || []; if (command.id === API_OPEN_EDITOR_COMMAND_ID || command.id === API_OPEN_DIFF_EDITOR_COMMAND_ID) { @@ -554,6 +549,17 @@ export class TreeView extends Disposable implements ITreeView { } + private async resolveCommand(element: ITreeItem | undefined): Promise { + let command = element?.command; + if (element && !command) { + if ((element instanceof ResolvableTreeItem) && element.hasResolve) { + await element.resolve(new CancellationTokenSource().token); + command = element.command; + } + } + return command; + } + private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent, actionRunner: MultipleSelectionActionRunner): void { this.hoverService.hideHover(); const node: ITreeItem | null = treeEvent.element; @@ -776,10 +782,17 @@ class TreeDataSource implements IAsyncDataSource { } async getChildren(element: ITreeItem): Promise { + let result: ITreeItem[] = []; if (this.treeView.dataProvider) { - return this.withProgress(this.treeView.dataProvider.getChildren(element)); + try { + result = await this.withProgress(this.treeView.dataProvider.getChildren(element)); + } catch (e) { + if (!(e.message).startsWith('Bad progress location:')) { + throw e; + } + } } - return []; + return result; } } @@ -840,6 +853,9 @@ class TreeRenderer extends Disposable implements ITreeRenderer { return this.hoverService.showHover(options); + }, + hideHover: () => { + return this.hoverService.hideHover(); } }; } @@ -868,7 +884,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer => { + markdown: (token: CancellationToken): Promise => { return new Promise(async (resolve) => { - await node.resolve(); + await node.resolve(token); resolve(node.tooltip); }); }, - markdownNotSupportedFallback: resource ? undefined : '' // Passing undefined as the fallback for a resource falls back to the old native hover + markdownNotSupportedFallback: resource ? undefined : label ?? '' // Passing undefined as the fallback for a resource falls back to the old native hover }; } @@ -928,7 +944,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer()); - readonly onDidChangeTitle: Event = this._onDidChangeTitle.event; - - constructor( - viewId: string, - menuId: MenuId, - contextMenuId: MenuId, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IMenuService private readonly menuService: IMenuService, - ) { - super(); - - const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); - scopedContextKeyService.createKey('view', viewId); - - const menu = this._register(this.menuService.createMenu(menuId, scopedContextKeyService)); - const updateActions = () => { - this.primaryActions = []; - this.secondaryActions = []; - this.titleActionsDisposable.value = createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, { primary: this.primaryActions, secondary: this.secondaryActions }); - this._onDidChangeTitle.fire(); - }; - this._register(menu.onDidChange(updateActions)); - updateActions(); - - const contextMenu = this._register(this.menuService.createMenu(contextMenuId, scopedContextKeyService)); - const updateContextMenuActions = () => { - this.contextMenuActions = []; - this.titleActionsDisposable.value = createAndFillInActionBarActions(contextMenu, { shouldForwardArgs: true }, { primary: [], secondary: this.contextMenuActions }); - }; - this._register(contextMenu.onDidChange(updateContextMenuActions)); - updateContextMenuActions(); - - this._register(toDisposable(() => { - this.primaryActions = []; - this.secondaryActions = []; - this.contextMenuActions = []; - })); - } - - getPrimaryActions(): IAction[] { - return this.primaryActions; - } - - getSecondaryActions(): IAction[] { - return this.secondaryActions; - } - - getContextMenuActions(): IAction[] { - return this.contextMenuActions; - } -} - -export class ViewContainerMenuActions extends Disposable { - - private readonly titleActionsDisposable = this._register(new MutableDisposable()); - private contextMenuActions: IAction[] = []; - - constructor( - containerId: string, - contextMenuId: MenuId, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IMenuService private readonly menuService: IMenuService, - ) { - super(); - - const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); - scopedContextKeyService.createKey('container', containerId); - - const contextMenu = this._register(this.menuService.createMenu(contextMenuId, scopedContextKeyService)); - const updateContextMenuActions = () => { - this.contextMenuActions = []; - this.titleActionsDisposable.value = createAndFillInActionBarActions(contextMenu, { shouldForwardArgs: true }, { primary: [], secondary: this.contextMenuActions }); - }; - this._register(contextMenu.onDidChange(updateContextMenuActions)); - updateContextMenuActions(); - - this._register(toDisposable(() => { - this.contextMenuActions = []; - })); - } - - getContextMenuActions(): IAction[] { - return this.contextMenuActions; - } -} diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts new file mode 100644 index 000000000..27f5af3cf --- /dev/null +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -0,0 +1,642 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/paneviewlet'; +import * as nls from 'vs/nls'; +import { Event, Emitter } from 'vs/base/common/event'; +import { foreground } from 'vs/platform/theme/common/colorRegistry'; +import { attachButtonStyler, attachLinkStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; +import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl } from 'vs/base/browser/dom'; +import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IAction, IActionViewItem } from 'vs/base/common/actions'; +import { ActionsOrientation, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IPaneOptions, Pane, IPaneStyles } from 'vs/base/browser/ui/splitview/paneview'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewDescriptorService, ViewContainerLocation, IViewsRegistry, IViewContentDescriptor, defaultViewIcon, IViewsService, ViewContainerLocationToString } from 'vs/workbench/common/views'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { assertIsDefined } from 'vs/base/common/types'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { MenuId, Action2, IAction2Options, IMenuService } from 'vs/platform/actions/common/actions'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { parseLinkedText } from 'vs/base/common/linkedText'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Link } from 'vs/platform/opener/browser/link'; +import { Orientation } from 'vs/base/browser/ui/sash/sash'; +import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; +import { CompositeProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; +import { IProgressIndicator } from 'vs/platform/progress/common/progress'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { URI } from 'vs/base/common/uri'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { Codicon } from 'vs/base/common/codicons'; +import { CompositeMenuActions } from 'vs/workbench/browser/menuActions'; + +export interface IViewPaneOptions extends IPaneOptions { + id: string; + showActionsAlways?: boolean; + titleMenuId?: MenuId; +} + +type WelcomeActionClassification = { + viewId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + uri: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; +}; + +const viewPaneContainerExpandedIcon = registerIcon('view-pane-container-expanded', Codicon.chevronDown, nls.localize('viewPaneContainerExpandedIcon', 'Icon for an expanded view pane container.')); +const viewPaneContainerCollapsedIcon = registerIcon('view-pane-container-collapsed', Codicon.chevronRight, nls.localize('viewPaneContainerCollapsedIcon', 'Icon for a collapsed view pane container.')); + +const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); + +interface IItem { + readonly descriptor: IViewContentDescriptor; + visible: boolean; +} + +class ViewWelcomeController { + + private _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + private defaultItem: IItem | undefined; + private items: IItem[] = []; + get contents(): IViewContentDescriptor[] { + const visibleItems = this.items.filter(v => v.visible); + + if (visibleItems.length === 0 && this.defaultItem) { + return [this.defaultItem.descriptor]; + } + + return visibleItems.map(v => v.descriptor); + } + + private contextKeyService: IContextKeyService; + private disposables = new DisposableStore(); + + constructor( + private id: string, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + this.contextKeyService = contextKeyService.createScoped(); + this.disposables.add(this.contextKeyService); + + contextKeyService.onDidChangeContext(this.onDidChangeContext, this, this.disposables); + Event.filter(viewsRegistry.onDidChangeViewWelcomeContent, id => id === this.id)(this.onDidChangeViewWelcomeContent, this, this.disposables); + this.onDidChangeViewWelcomeContent(); + } + + private onDidChangeViewWelcomeContent(): void { + const descriptors = viewsRegistry.getViewWelcomeContent(this.id); + + this.items = []; + + for (const descriptor of descriptors) { + if (descriptor.when === 'default') { + this.defaultItem = { descriptor, visible: true }; + } else { + const visible = descriptor.when ? this.contextKeyService.contextMatchesRules(descriptor.when) : true; + this.items.push({ descriptor, visible }); + } + } + + this._onDidChange.fire(); + } + + private onDidChangeContext(): void { + let didChange = false; + + for (const item of this.items) { + if (!item.descriptor.when || item.descriptor.when === 'default') { + continue; + } + + const visible = this.contextKeyService.contextMatchesRules(item.descriptor.when); + + if (item.visible === visible) { + continue; + } + + item.visible = visible; + didChange = true; + } + + if (didChange) { + this._onDidChange.fire(); + } + } + + dispose(): void { + this.disposables.dispose(); + } +} + +class ViewMenuActions extends CompositeMenuActions { + constructor( + viewId: string, + menuId: MenuId, + contextMenuId: MenuId, + @IContextKeyService contextKeyService: IContextKeyService, + @IMenuService menuService: IMenuService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + ) { + const scopedContextKeyService = contextKeyService.createScoped(); + scopedContextKeyService.createKey('view', viewId); + const viewLocationKey = scopedContextKeyService.createKey('viewLocation', ViewContainerLocationToString(viewDescriptorService.getViewLocationById(viewId)!)); + super(menuId, contextMenuId, { shouldForwardArgs: true }, scopedContextKeyService, menuService); + this._register(scopedContextKeyService); + this._register(Event.filter(viewDescriptorService.onDidChangeLocation, e => e.views.some(view => view.id === viewId))(() => viewLocationKey.set(ViewContainerLocationToString(viewDescriptorService.getViewLocationById(viewId)!)))); + } + +} + +export abstract class ViewPane extends Pane implements IView { + + private static readonly AlwaysShowActionsConfig = 'workbench.view.alwaysShowHeaderActions'; + + private _onDidFocus = this._register(new Emitter()); + readonly onDidFocus: Event = this._onDidFocus.event; + + private _onDidBlur = this._register(new Emitter()); + readonly onDidBlur: Event = this._onDidBlur.event; + + private _onDidChangeBodyVisibility = this._register(new Emitter()); + readonly onDidChangeBodyVisibility: Event = this._onDidChangeBodyVisibility.event; + + protected _onDidChangeTitleArea = this._register(new Emitter()); + readonly onDidChangeTitleArea: Event = this._onDidChangeTitleArea.event; + + protected _onDidChangeViewWelcomeState = this._register(new Emitter()); + readonly onDidChangeViewWelcomeState: Event = this._onDidChangeViewWelcomeState.event; + + private focusedViewContextKey: IContextKey; + + private _isVisible: boolean = false; + readonly id: string; + + private _title: string; + public get title(): string { + return this._title; + } + + private _titleDescription: string | undefined; + public get titleDescription(): string | undefined { + return this._titleDescription; + } + + private readonly menuActions: ViewMenuActions; + private progressBar!: ProgressBar; + private progressIndicator!: IProgressIndicator; + + private toolbar?: ToolBar; + private readonly showActionsAlways: boolean = false; + private headerContainer?: HTMLElement; + private titleContainer?: HTMLElement; + private titleDescriptionContainer?: HTMLElement; + private iconContainer?: HTMLElement; + protected twistiesContainer?: HTMLElement; + + private bodyContainer!: HTMLElement; + private viewWelcomeContainer!: HTMLElement; + private viewWelcomeDisposable: IDisposable = Disposable.None; + private viewWelcomeController: ViewWelcomeController; + + constructor( + options: IViewPaneOptions, + @IKeybindingService protected keybindingService: IKeybindingService, + @IContextMenuService protected contextMenuService: IContextMenuService, + @IConfigurationService protected readonly configurationService: IConfigurationService, + @IContextKeyService protected contextKeyService: IContextKeyService, + @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService, + @IInstantiationService protected instantiationService: IInstantiationService, + @IOpenerService protected openerService: IOpenerService, + @IThemeService protected themeService: IThemeService, + @ITelemetryService protected telemetryService: ITelemetryService, + ) { + super({ ...options, ...{ orientation: viewDescriptorService.getViewLocationById(options.id) === ViewContainerLocation.Panel ? Orientation.HORIZONTAL : Orientation.VERTICAL } }); + + this.id = options.id; + this._title = options.title; + this._titleDescription = options.titleDescription; + this.showActionsAlways = !!options.showActionsAlways; + this.focusedViewContextKey = FocusedViewContext.bindTo(contextKeyService); + + this.menuActions = this._register(instantiationService.createInstance(ViewMenuActions, this.id, options.titleMenuId || MenuId.ViewTitle, MenuId.ViewTitleContext)); + this._register(this.menuActions.onDidChange(() => this.updateActions())); + + this.viewWelcomeController = new ViewWelcomeController(this.id, contextKeyService); + } + + get headerVisible(): boolean { + return super.headerVisible; + } + + set headerVisible(visible: boolean) { + super.headerVisible = visible; + this.element.classList.toggle('merged-header', !visible); + } + + setVisible(visible: boolean): void { + if (this._isVisible !== visible) { + this._isVisible = visible; + + if (this.isExpanded()) { + this._onDidChangeBodyVisibility.fire(visible); + } + } + } + + isVisible(): boolean { + return this._isVisible; + } + + isBodyVisible(): boolean { + return this._isVisible && this.isExpanded(); + } + + setExpanded(expanded: boolean): boolean { + const changed = super.setExpanded(expanded); + if (changed) { + this._onDidChangeBodyVisibility.fire(expanded); + } + if (this.twistiesContainer) { + this.twistiesContainer.classList.remove(...ThemeIcon.asClassNameArray(this.getTwistyIcon(!expanded))); + this.twistiesContainer.classList.add(...ThemeIcon.asClassNameArray(this.getTwistyIcon(expanded))); + } + return changed; + } + + render(): void { + super.render(); + + const focusTracker = trackFocus(this.element); + this._register(focusTracker); + this._register(focusTracker.onDidFocus(() => { + this.focusedViewContextKey.set(this.id); + this._onDidFocus.fire(); + })); + this._register(focusTracker.onDidBlur(() => { + if (this.focusedViewContextKey.get() === this.id) { + this.focusedViewContextKey.reset(); + } + + this._onDidBlur.fire(); + })); + } + + protected renderHeader(container: HTMLElement): void { + this.headerContainer = container; + + this.twistiesContainer = append(container, $(ThemeIcon.asCSSSelector(this.getTwistyIcon(this.isExpanded())))); + + this.renderHeaderTitle(container, this.title); + + const actions = append(container, $('.actions')); + actions.classList.toggle('show', this.showActionsAlways); + this.toolbar = new ToolBar(actions, this.contextMenuService, { + orientation: ActionsOrientation.HORIZONTAL, + actionViewItemProvider: action => this.getActionViewItem(action), + ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.title), + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + renderDropdownAsChildElement: true + }); + + this._register(this.toolbar); + this.setActions(); + + this._register(addDisposableListener(actions, EventType.CLICK, e => e.preventDefault())); + + this._register(this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!.onDidChangeContainerInfo(({ title }) => { + this.updateTitle(this.title); + })); + + const onDidRelevantConfigurationChange = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ViewPane.AlwaysShowActionsConfig)); + this._register(onDidRelevantConfigurationChange(this.updateActionsVisibility, this)); + this.updateActionsVisibility(); + } + + protected getTwistyIcon(expanded: boolean): ThemeIcon { + return expanded ? viewPaneContainerExpandedIcon : viewPaneContainerCollapsedIcon; + } + + style(styles: IPaneStyles): void { + super.style(styles); + + const icon = this.getIcon(); + if (this.iconContainer) { + const fgColor = styles.headerForeground || this.themeService.getColorTheme().getColor(foreground); + if (URI.isUri(icon)) { + // Apply background color to activity bar item provided with iconUrls + this.iconContainer.style.backgroundColor = fgColor ? fgColor.toString() : ''; + this.iconContainer.style.color = ''; + } else { + // Apply foreground color to activity bar items provided with codicons + this.iconContainer.style.color = fgColor ? fgColor.toString() : ''; + this.iconContainer.style.backgroundColor = ''; + } + } + } + + private getIcon(): ThemeIcon | URI { + return this.viewDescriptorService.getViewDescriptorById(this.id)?.containerIcon || defaultViewIcon; + } + + protected renderHeaderTitle(container: HTMLElement, title: string): void { + this.iconContainer = append(container, $('.icon', undefined)); + const icon = this.getIcon(); + + let cssClass: string | undefined = undefined; + if (URI.isUri(icon)) { + cssClass = `view-${this.id.replace(/[\.\:]/g, '-')}`; + const iconClass = `.pane-header .icon.${cssClass}`; + + createCSSRule(iconClass, ` + mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + mask-size: 24px; + -webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + -webkit-mask-size: 16px; + `); + } else if (ThemeIcon.isThemeIcon(icon)) { + cssClass = ThemeIcon.asClassName(icon); + } + + if (cssClass) { + this.iconContainer.classList.add(...cssClass.split(' ')); + } + + const calculatedTitle = this.calculateTitle(title); + this.titleContainer = append(container, $('h3.title', { title: calculatedTitle }, calculatedTitle)); + + if (this._titleDescription) { + this.setTitleDescription(this._titleDescription); + } + + this.iconContainer.title = calculatedTitle; + this.iconContainer.setAttribute('aria-label', calculatedTitle); + } + + protected updateTitle(title: string): void { + const calculatedTitle = this.calculateTitle(title); + if (this.titleContainer) { + this.titleContainer.textContent = calculatedTitle; + this.titleContainer.setAttribute('title', calculatedTitle); + } + + if (this.iconContainer) { + this.iconContainer.title = calculatedTitle; + this.iconContainer.setAttribute('aria-label', calculatedTitle); + } + + this._title = title; + this._onDidChangeTitleArea.fire(); + } + + private setTitleDescription(description: string | undefined) { + if (this.titleDescriptionContainer) { + this.titleDescriptionContainer.textContent = description ?? ''; + this.titleDescriptionContainer.setAttribute('title', description ?? ''); + } + else if (description && this.titleContainer) { + this.titleDescriptionContainer = after(this.titleContainer, $('span.description', { title: description }, description)); + } + } + + protected updateTitleDescription(description?: string | undefined): void { + this.setTitleDescription(description); + + this._titleDescription = description; + this._onDidChangeTitleArea.fire(); + } + + private calculateTitle(title: string): string { + const viewContainer = this.viewDescriptorService.getViewContainerByViewId(this.id)!; + const model = this.viewDescriptorService.getViewContainerModel(viewContainer); + const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.id); + const isDefault = this.viewDescriptorService.getDefaultContainerById(this.id) === viewContainer; + + if (!isDefault && viewDescriptor?.containerTitle && model.title !== viewDescriptor.containerTitle) { + return `${viewDescriptor.containerTitle}: ${title}`; + } + + return title; + } + + private scrollableElement!: DomScrollableElement; + + protected renderBody(container: HTMLElement): void { + this.bodyContainer = container; + + const viewWelcomeContainer = append(container, $('.welcome-view')); + this.viewWelcomeContainer = $('.welcome-view-content', { tabIndex: 0 }); + this.scrollableElement = this._register(new DomScrollableElement(this.viewWelcomeContainer, { + alwaysConsumeMouseWheel: true, + horizontal: ScrollbarVisibility.Hidden, + vertical: ScrollbarVisibility.Visible, + })); + + append(viewWelcomeContainer, this.scrollableElement.getDomNode()); + + const onViewWelcomeChange = Event.any(this.viewWelcomeController.onDidChange, this.onDidChangeViewWelcomeState); + this._register(onViewWelcomeChange(this.updateViewWelcome, this)); + this.updateViewWelcome(); + } + + protected layoutBody(height: number, width: number): void { + this.viewWelcomeContainer.style.height = `${height}px`; + this.viewWelcomeContainer.style.width = `${width}px`; + this.scrollableElement.scanDomNode(); + } + + getProgressIndicator() { + if (this.progressBar === undefined) { + // Progress bar + this.progressBar = this._register(new ProgressBar(this.element)); + this._register(attachProgressBarStyler(this.progressBar, this.themeService)); + this.progressBar.hide(); + } + + if (this.progressIndicator === undefined) { + this.progressIndicator = this.instantiationService.createInstance(CompositeProgressIndicator, assertIsDefined(this.progressBar), this.id, this.isBodyVisible()); + } + return this.progressIndicator; + } + + protected getProgressLocation(): string { + return this.viewDescriptorService.getViewContainerByViewId(this.id)!.id; + } + + protected getBackgroundColor(): string { + return this.viewDescriptorService.getViewLocationById(this.id) === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND; + } + + focus(): void { + if (this.shouldShowWelcome()) { + this.viewWelcomeContainer.focus(); + } else if (this.element) { + this.element.focus(); + this._onDidFocus.fire(); + } + } + + private setActions(): void { + if (this.toolbar) { + this.toolbar.setActions(prepareActions(this.getActions()), prepareActions(this.getSecondaryActions())); + this.toolbar.context = this.getActionsContext(); + } + } + + private updateActionsVisibility(): void { + if (!this.headerContainer) { + return; + } + const shouldAlwaysShowActions = this.configurationService.getValue('workbench.view.alwaysShowHeaderActions'); + this.headerContainer.classList.toggle('actions-always-visible', shouldAlwaysShowActions); + } + + protected updateActions(): void { + this.setActions(); + this._onDidChangeTitleArea.fire(); + } + + getActions(): IAction[] { + return this.menuActions.getPrimaryActions(); + } + + getSecondaryActions(): IAction[] { + return this.menuActions.getSecondaryActions(); + } + + getContextMenuActions(): IAction[] { + return this.menuActions.getContextMenuActions(); + } + + getActionViewItem(action: IAction): IActionViewItem | undefined { + return createActionViewItem(this.instantiationService, action); + } + + getActionsContext(): unknown { + return undefined; + } + + getOptimalWidth(): number { + return 0; + } + + saveState(): void { + // Subclasses to implement for saving state + } + + private updateViewWelcome(): void { + this.viewWelcomeDisposable.dispose(); + + if (!this.shouldShowWelcome()) { + this.bodyContainer.classList.remove('welcome'); + this.viewWelcomeContainer.innerText = ''; + this.scrollableElement.scanDomNode(); + return; + } + + const contents = this.viewWelcomeController.contents; + + if (contents.length === 0) { + this.bodyContainer.classList.remove('welcome'); + this.viewWelcomeContainer.innerText = ''; + this.scrollableElement.scanDomNode(); + return; + } + + const disposables = new DisposableStore(); + this.bodyContainer.classList.add('welcome'); + this.viewWelcomeContainer.innerText = ''; + + for (const { content, precondition } of contents) { + const lines = content.split('\n'); + + for (let line of lines) { + line = line.trim(); + + if (!line) { + continue; + } + + const linkedText = parseLinkedText(line); + + if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') { + const node = linkedText.nodes[0]; + const button = new Button(this.viewWelcomeContainer, { title: node.title, supportIcons: true }); + button.label = node.label; + button.onDidClick(_ => { + this.telemetryService.publicLog2<{ viewId: string, uri: string }, WelcomeActionClassification>('views.welcomeAction', { viewId: this.id, uri: node.href }); + this.openerService.open(node.href); + }, null, disposables); + disposables.add(button); + disposables.add(attachButtonStyler(button, this.themeService)); + + if (precondition) { + const updateEnablement = () => button.enabled = this.contextKeyService.contextMatchesRules(precondition); + updateEnablement(); + + const keys = new Set(); + precondition.keys().forEach(key => keys.add(key)); + const onDidChangeContext = Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(keys)); + onDidChangeContext(updateEnablement, null, disposables); + } + } else { + const p = append(this.viewWelcomeContainer, $('p')); + + for (const node of linkedText.nodes) { + if (typeof node === 'string') { + append(p, document.createTextNode(node)); + } else { + const link = this.instantiationService.createInstance(Link, node); + append(p, link.el); + disposables.add(link); + disposables.add(attachLinkStyler(link, this.themeService)); + + if (precondition && node.href.startsWith('command:')) { + const updateEnablement = () => link.style({ disabled: !this.contextKeyService.contextMatchesRules(precondition) }); + updateEnablement(); + + const keys = new Set(); + precondition.keys().forEach(key => keys.add(key)); + const onDidChangeContext = Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(keys)); + onDidChangeContext(updateEnablement, null, disposables); + } + } + } + } + } + } + + this.scrollableElement.scanDomNode(); + this.viewWelcomeDisposable = disposables; + } + + shouldShowWelcome(): boolean { + return false; + } +} + +export abstract class ViewAction extends Action2 { + constructor(readonly desc: Readonly & { viewId: string }) { + super(desc); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const view = accessor.get(IViewsService).getActiveViewWithId(this.desc.viewId); + if (view) { + return this.runInView(accessor, view, ...args); + } + } + + abstract runInView(accessor: ServicesAccessor, view: T, ...args: any[]): any; +} diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 410c67244..e71e07d07 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -6,52 +6,45 @@ import 'vs/css!./media/paneviewlet'; import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import { ColorIdentifier, activeContrastBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; -import { attachStyler, IColorMapping, attachButtonStyler, attachLinkStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; -import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, PANEL_BACKGROUND, SIDE_BAR_BACKGROUND, PANEL_SECTION_HEADER_FOREGROUND, PANEL_SECTION_HEADER_BACKGROUND, PANEL_SECTION_HEADER_BORDER, PANEL_SECTION_DRAG_AND_DROP_BACKGROUND, PANEL_SECTION_BORDER } from 'vs/workbench/common/theme'; -import { after, append, $, trackFocus, EventType, isAncestor, Dimension, addDisposableListener, createCSSRule, asCSSUrl } from 'vs/base/browser/dom'; -import { IDisposable, combinedDisposable, dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IAction, Separator, IActionViewItem } from 'vs/base/common/actions'; -import { ActionsOrientation, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ColorIdentifier, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { attachStyler, IColorMapping } from 'vs/platform/theme/common/styler'; +import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, PANEL_SECTION_HEADER_FOREGROUND, PANEL_SECTION_HEADER_BACKGROUND, PANEL_SECTION_HEADER_BORDER, PANEL_SECTION_DRAG_AND_DROP_BACKGROUND, PANEL_SECTION_BORDER } from 'vs/workbench/common/theme'; +import { EventType, Dimension, addDisposableListener, isAncestor } from 'vs/base/browser/dom'; +import { IDisposable, combinedDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { IAction, IActionViewItem, Separator } from 'vs/base/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { PaneView, IPaneViewOptions, IPaneOptions, Pane, IPaneStyles } from 'vs/base/browser/ui/splitview/paneview'; +import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; +import { PaneView, IPaneViewOptions } from 'vs/base/browser/ui/splitview/paneview'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer, IViewsRegistry, IViewContentDescriptor, IAddedViewDescriptorRef, IViewDescriptorRef, IViewContainerModel, defaultViewIcon } from 'vs/workbench/common/views'; +import { IView, FocusedViewContext, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer, IAddedViewDescriptorRef, IViewDescriptorRef, IViewContainerModel, IViewsService, ViewContainerLocationToString } from 'vs/workbench/common/views'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyEqualsExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { assertIsDefined } from 'vs/base/common/types'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Component } from 'vs/workbench/common/component'; -import { MenuId, MenuItemAction, registerAction2, Action2, IAction2Options, SubmenuItemAction } from 'vs/platform/actions/common/actions'; -import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; -import { parseLinkedText } from 'vs/base/common/linkedText'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { Button } from 'vs/base/browser/ui/button/button'; -import { Link } from 'vs/platform/opener/browser/link'; +import { registerAction2, Action2, IAction2Options, IMenuService, MenuId, MenuRegistry, ISubmenuItem, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { CompositeDragAndDropObserver, DragAndDropObserver, toggleDropEffect } from 'vs/workbench/browser/dnd'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; -import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; -import { CompositeProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; -import { IProgressIndicator } from 'vs/platform/progress/common/progress'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { ScrollbarVisibility } from 'vs/base/common/scrollable'; -import { URI } from 'vs/base/common/uri'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -import { Codicon } from 'vs/base/common/codicons'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { CompositeMenuActions } from 'vs/workbench/browser/menuActions'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; + +export const ViewsSubMenu = new MenuId('Views'); +MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + submenu: ViewsSubMenu, + title: nls.localize('views', "Views"), + order: 1, + when: ContextKeyEqualsExpr.create('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar)), +}); export interface IPaneColors extends IColorMapping { dropBackground?: ColorIdentifier; @@ -61,566 +54,6 @@ export interface IPaneColors extends IColorMapping { leftBorder?: ColorIdentifier; } -export interface IViewPaneOptions extends IPaneOptions { - id: string; - showActionsAlways?: boolean; - titleMenuId?: MenuId; -} - -type WelcomeActionClassification = { - viewId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - uri: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; -}; - -const viewPaneContainerExpandedIcon = registerIcon('view-pane-container-expanded', Codicon.chevronDown, nls.localize('viewPaneContainerExpandedIcon', 'Icon for an expanded view pane container.')); -const viewPaneContainerCollapsedIcon = registerIcon('view-pane-container-collapsed', Codicon.chevronRight, nls.localize('viewPaneContainerCollapsedIcon', 'Icon for a collapsed view pane container.')); - -const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); - -interface IItem { - readonly descriptor: IViewContentDescriptor; - visible: boolean; -} - -class ViewWelcomeController { - - private _onDidChange = new Emitter(); - readonly onDidChange = this._onDidChange.event; - - private defaultItem: IItem | undefined; - private items: IItem[] = []; - get contents(): IViewContentDescriptor[] { - const visibleItems = this.items.filter(v => v.visible); - - if (visibleItems.length === 0 && this.defaultItem) { - return [this.defaultItem.descriptor]; - } - - return visibleItems.map(v => v.descriptor); - } - - private contextKeyService: IContextKeyService; - private disposables = new DisposableStore(); - - constructor( - private id: string, - @IContextKeyService contextKeyService: IContextKeyService, - ) { - this.contextKeyService = contextKeyService.createScoped(); - this.disposables.add(this.contextKeyService); - - contextKeyService.onDidChangeContext(this.onDidChangeContext, this, this.disposables); - Event.filter(viewsRegistry.onDidChangeViewWelcomeContent, id => id === this.id)(this.onDidChangeViewWelcomeContent, this, this.disposables); - this.onDidChangeViewWelcomeContent(); - } - - private onDidChangeViewWelcomeContent(): void { - const descriptors = viewsRegistry.getViewWelcomeContent(this.id); - - this.items = []; - - for (const descriptor of descriptors) { - if (descriptor.when === 'default') { - this.defaultItem = { descriptor, visible: true }; - } else { - const visible = descriptor.when ? this.contextKeyService.contextMatchesRules(descriptor.when) : true; - this.items.push({ descriptor, visible }); - } - } - - this._onDidChange.fire(); - } - - private onDidChangeContext(): void { - let didChange = false; - - for (const item of this.items) { - if (!item.descriptor.when || item.descriptor.when === 'default') { - continue; - } - - const visible = this.contextKeyService.contextMatchesRules(item.descriptor.when); - - if (item.visible === visible) { - continue; - } - - item.visible = visible; - didChange = true; - } - - if (didChange) { - this._onDidChange.fire(); - } - } - - dispose(): void { - this.disposables.dispose(); - } -} - -export abstract class ViewPane extends Pane implements IView { - - private static readonly AlwaysShowActionsConfig = 'workbench.view.alwaysShowHeaderActions'; - - private _onDidFocus = this._register(new Emitter()); - readonly onDidFocus: Event = this._onDidFocus.event; - - private _onDidBlur = this._register(new Emitter()); - readonly onDidBlur: Event = this._onDidBlur.event; - - private _onDidChangeBodyVisibility = this._register(new Emitter()); - readonly onDidChangeBodyVisibility: Event = this._onDidChangeBodyVisibility.event; - - protected _onDidChangeTitleArea = this._register(new Emitter()); - readonly onDidChangeTitleArea: Event = this._onDidChangeTitleArea.event; - - protected _onDidChangeViewWelcomeState = this._register(new Emitter()); - readonly onDidChangeViewWelcomeState: Event = this._onDidChangeViewWelcomeState.event; - - private focusedViewContextKey: IContextKey; - - private _isVisible: boolean = false; - readonly id: string; - - private _title: string; - public get title(): string { - return this._title; - } - - private _titleDescription: string | undefined; - public get titleDescription(): string | undefined { - return this._titleDescription; - } - - private readonly menuActions: ViewMenuActions; - private progressBar!: ProgressBar; - private progressIndicator!: IProgressIndicator; - - private toolbar?: ToolBar; - private readonly showActionsAlways: boolean = false; - private headerContainer?: HTMLElement; - private titleContainer?: HTMLElement; - private titleDescriptionContainer?: HTMLElement; - private iconContainer?: HTMLElement; - protected twistiesContainer?: HTMLElement; - - private bodyContainer!: HTMLElement; - private viewWelcomeContainer!: HTMLElement; - private viewWelcomeDisposable: IDisposable = Disposable.None; - private viewWelcomeController: ViewWelcomeController; - - constructor( - options: IViewPaneOptions, - @IKeybindingService protected keybindingService: IKeybindingService, - @IContextMenuService protected contextMenuService: IContextMenuService, - @IConfigurationService protected readonly configurationService: IConfigurationService, - @IContextKeyService protected contextKeyService: IContextKeyService, - @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService, - @IInstantiationService protected instantiationService: IInstantiationService, - @IOpenerService protected openerService: IOpenerService, - @IThemeService protected themeService: IThemeService, - @ITelemetryService protected telemetryService: ITelemetryService, - ) { - super({ ...options, ...{ orientation: viewDescriptorService.getViewLocationById(options.id) === ViewContainerLocation.Panel ? Orientation.HORIZONTAL : Orientation.VERTICAL } }); - - this.id = options.id; - this._title = options.title; - this._titleDescription = options.titleDescription; - this.showActionsAlways = !!options.showActionsAlways; - this.focusedViewContextKey = FocusedViewContext.bindTo(contextKeyService); - - this.menuActions = this._register(instantiationService.createInstance(ViewMenuActions, this.id, options.titleMenuId || MenuId.ViewTitle, MenuId.ViewTitleContext)); - this._register(this.menuActions.onDidChangeTitle(() => this.updateActions())); - - this.viewWelcomeController = new ViewWelcomeController(this.id, contextKeyService); - } - - get headerVisible(): boolean { - return super.headerVisible; - } - - set headerVisible(visible: boolean) { - super.headerVisible = visible; - this.element.classList.toggle('merged-header', !visible); - } - - setVisible(visible: boolean): void { - if (this._isVisible !== visible) { - this._isVisible = visible; - - if (this.isExpanded()) { - this._onDidChangeBodyVisibility.fire(visible); - } - } - } - - isVisible(): boolean { - return this._isVisible; - } - - isBodyVisible(): boolean { - return this._isVisible && this.isExpanded(); - } - - setExpanded(expanded: boolean): boolean { - const changed = super.setExpanded(expanded); - if (changed) { - this._onDidChangeBodyVisibility.fire(expanded); - } - if (this.twistiesContainer) { - this.twistiesContainer.classList.remove(...ThemeIcon.asClassNameArray(this.getTwistyIcon(!expanded))); - this.twistiesContainer.classList.add(...ThemeIcon.asClassNameArray(this.getTwistyIcon(expanded))); - } - return changed; - } - - render(): void { - super.render(); - - const focusTracker = trackFocus(this.element); - this._register(focusTracker); - this._register(focusTracker.onDidFocus(() => { - this.focusedViewContextKey.set(this.id); - this._onDidFocus.fire(); - })); - this._register(focusTracker.onDidBlur(() => { - if (this.focusedViewContextKey.get() === this.id) { - this.focusedViewContextKey.reset(); - } - - this._onDidBlur.fire(); - })); - } - - protected renderHeader(container: HTMLElement): void { - this.headerContainer = container; - - this.twistiesContainer = append(container, $(ThemeIcon.asCSSSelector(this.getTwistyIcon(this.isExpanded())))); - - this.renderHeaderTitle(container, this.title); - - const actions = append(container, $('.actions')); - actions.classList.toggle('show', this.showActionsAlways); - this.toolbar = new ToolBar(actions, this.contextMenuService, { - orientation: ActionsOrientation.HORIZONTAL, - actionViewItemProvider: action => this.getActionViewItem(action), - ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.title), - getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), - renderDropdownAsChildElement: true - }); - - this._register(this.toolbar); - this.setActions(); - - this._register(addDisposableListener(actions, EventType.CLICK, e => e.preventDefault())); - - this._register(this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!.onDidChangeContainerInfo(({ title }) => { - this.updateTitle(this.title); - })); - - const onDidRelevantConfigurationChange = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ViewPane.AlwaysShowActionsConfig)); - this._register(onDidRelevantConfigurationChange(this.updateActionsVisibility, this)); - this.updateActionsVisibility(); - } - - protected getTwistyIcon(expanded: boolean): ThemeIcon { - return expanded ? viewPaneContainerExpandedIcon : viewPaneContainerCollapsedIcon; - } - - style(styles: IPaneStyles): void { - super.style(styles); - - const icon = this.getIcon(); - if (this.iconContainer) { - const fgColor = styles.headerForeground || this.themeService.getColorTheme().getColor(foreground); - if (URI.isUri(icon)) { - // Apply background color to activity bar item provided with iconUrls - this.iconContainer.style.backgroundColor = fgColor ? fgColor.toString() : ''; - this.iconContainer.style.color = ''; - } else { - // Apply foreground color to activity bar items provided with codicons - this.iconContainer.style.color = fgColor ? fgColor.toString() : ''; - this.iconContainer.style.backgroundColor = ''; - } - } - } - - private getIcon(): ThemeIcon | URI { - return this.viewDescriptorService.getViewDescriptorById(this.id)?.containerIcon || defaultViewIcon; - } - - protected renderHeaderTitle(container: HTMLElement, title: string): void { - this.iconContainer = append(container, $('.icon', undefined)); - const icon = this.getIcon(); - - let cssClass: string | undefined = undefined; - if (URI.isUri(icon)) { - cssClass = `view-${this.id.replace(/[\.\:]/g, '-')}`; - const iconClass = `.pane-header .icon.${cssClass}`; - - createCSSRule(iconClass, ` - mask: ${asCSSUrl(icon)} no-repeat 50% 50%; - mask-size: 24px; - -webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%; - -webkit-mask-size: 16px; - `); - } else if (ThemeIcon.isThemeIcon(icon)) { - cssClass = ThemeIcon.asClassName(icon); - } - - if (cssClass) { - this.iconContainer.classList.add(...cssClass.split(' ')); - } - - const calculatedTitle = this.calculateTitle(title); - this.titleContainer = append(container, $('h3.title', { title: calculatedTitle }, calculatedTitle)); - - if (this._titleDescription) { - this.setTitleDescription(this._titleDescription); - } - - this.iconContainer.title = calculatedTitle; - this.iconContainer.setAttribute('aria-label', calculatedTitle); - } - - protected updateTitle(title: string): void { - const calculatedTitle = this.calculateTitle(title); - if (this.titleContainer) { - this.titleContainer.textContent = calculatedTitle; - this.titleContainer.setAttribute('title', calculatedTitle); - } - - if (this.iconContainer) { - this.iconContainer.title = calculatedTitle; - this.iconContainer.setAttribute('aria-label', calculatedTitle); - } - - this._title = title; - this._onDidChangeTitleArea.fire(); - } - - private setTitleDescription(description: string | undefined) { - if (this.titleDescriptionContainer) { - this.titleDescriptionContainer.textContent = description ?? ''; - this.titleDescriptionContainer.setAttribute('title', description ?? ''); - } - else if (description && this.titleContainer) { - this.titleDescriptionContainer = after(this.titleContainer, $('span.description', { title: description }, description)); - } - } - - protected updateTitleDescription(description?: string | undefined): void { - this.setTitleDescription(description); - - this._titleDescription = description; - this._onDidChangeTitleArea.fire(); - } - - private calculateTitle(title: string): string { - const viewContainer = this.viewDescriptorService.getViewContainerByViewId(this.id)!; - const model = this.viewDescriptorService.getViewContainerModel(viewContainer); - const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.id); - const isDefault = this.viewDescriptorService.getDefaultContainerById(this.id) === viewContainer; - - if (!isDefault && viewDescriptor?.containerTitle && model.title !== viewDescriptor.containerTitle) { - return `${viewDescriptor.containerTitle}: ${title}`; - } - - return title; - } - - private scrollableElement!: DomScrollableElement; - - protected renderBody(container: HTMLElement): void { - this.bodyContainer = container; - - const viewWelcomeContainer = append(container, $('.welcome-view')); - this.viewWelcomeContainer = $('.welcome-view-content', { tabIndex: 0 }); - this.scrollableElement = this._register(new DomScrollableElement(this.viewWelcomeContainer, { - alwaysConsumeMouseWheel: true, - horizontal: ScrollbarVisibility.Hidden, - vertical: ScrollbarVisibility.Visible, - })); - - append(viewWelcomeContainer, this.scrollableElement.getDomNode()); - - const onViewWelcomeChange = Event.any(this.viewWelcomeController.onDidChange, this.onDidChangeViewWelcomeState); - this._register(onViewWelcomeChange(this.updateViewWelcome, this)); - this.updateViewWelcome(); - } - - protected layoutBody(height: number, width: number): void { - this.viewWelcomeContainer.style.height = `${height}px`; - this.viewWelcomeContainer.style.width = `${width}px`; - this.scrollableElement.scanDomNode(); - } - - getProgressIndicator() { - if (this.progressBar === undefined) { - // Progress bar - this.progressBar = this._register(new ProgressBar(this.element)); - this._register(attachProgressBarStyler(this.progressBar, this.themeService)); - this.progressBar.hide(); - } - - if (this.progressIndicator === undefined) { - this.progressIndicator = this.instantiationService.createInstance(CompositeProgressIndicator, assertIsDefined(this.progressBar), this.id, this.isBodyVisible()); - } - return this.progressIndicator; - } - - protected getProgressLocation(): string { - return this.viewDescriptorService.getViewContainerByViewId(this.id)!.id; - } - - protected getBackgroundColor(): string { - return this.viewDescriptorService.getViewLocationById(this.id) === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND; - } - - focus(): void { - if (this.shouldShowWelcome()) { - this.viewWelcomeContainer.focus(); - } else if (this.element) { - this.element.focus(); - this._onDidFocus.fire(); - } - } - - private setActions(): void { - if (this.toolbar) { - this.toolbar.setActions(prepareActions(this.getActions()), prepareActions(this.getSecondaryActions())); - this.toolbar.context = this.getActionsContext(); - } - } - - private updateActionsVisibility(): void { - if (!this.headerContainer) { - return; - } - const shouldAlwaysShowActions = this.configurationService.getValue('workbench.view.alwaysShowHeaderActions'); - this.headerContainer.classList.toggle('actions-always-visible', shouldAlwaysShowActions); - } - - protected updateActions(): void { - this.setActions(); - this._onDidChangeTitleArea.fire(); - } - - getActions(): IAction[] { - return this.menuActions.getPrimaryActions(); - } - - getSecondaryActions(): IAction[] { - return this.menuActions.getSecondaryActions(); - } - - getContextMenuActions(): IAction[] { - return this.menuActions.getContextMenuActions(); - } - - getActionViewItem(action: IAction): IActionViewItem | undefined { - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - return undefined; - } - - getActionsContext(): unknown { - return undefined; - } - - getOptimalWidth(): number { - return 0; - } - - saveState(): void { - // Subclasses to implement for saving state - } - - private updateViewWelcome(): void { - this.viewWelcomeDisposable.dispose(); - - if (!this.shouldShowWelcome()) { - this.bodyContainer.classList.remove('welcome'); - this.viewWelcomeContainer.innerText = ''; - this.scrollableElement.scanDomNode(); - return; - } - - const contents = this.viewWelcomeController.contents; - - if (contents.length === 0) { - this.bodyContainer.classList.remove('welcome'); - this.viewWelcomeContainer.innerText = ''; - this.scrollableElement.scanDomNode(); - return; - } - - const disposables = new DisposableStore(); - this.bodyContainer.classList.add('welcome'); - this.viewWelcomeContainer.innerText = ''; - - for (const { content, precondition } of contents) { - const lines = content.split('\n'); - - for (let line of lines) { - line = line.trim(); - - if (!line) { - continue; - } - - const linkedText = parseLinkedText(line); - - if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') { - const node = linkedText.nodes[0]; - const button = new Button(this.viewWelcomeContainer, { title: node.title, supportCodicons: true }); - button.label = node.label; - button.onDidClick(_ => { - this.telemetryService.publicLog2<{ viewId: string, uri: string }, WelcomeActionClassification>('views.welcomeAction', { viewId: this.id, uri: node.href }); - this.openerService.open(node.href); - }, null, disposables); - disposables.add(button); - disposables.add(attachButtonStyler(button, this.themeService)); - - if (precondition) { - const updateEnablement = () => button.enabled = this.contextKeyService.contextMatchesRules(precondition); - updateEnablement(); - - const keys = new Set(); - precondition.keys().forEach(key => keys.add(key)); - const onDidChangeContext = Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(keys)); - onDidChangeContext(updateEnablement, null, disposables); - } - } else { - const p = append(this.viewWelcomeContainer, $('p')); - - for (const node of linkedText.nodes) { - if (typeof node === 'string') { - append(p, document.createTextNode(node)); - } else { - const link = this.instantiationService.createInstance(Link, node); - append(p, link.el); - disposables.add(link); - disposables.add(attachLinkStyler(link, this.themeService)); - } - } - } - } - } - - this.scrollableElement.scanDomNode(); - this.viewWelcomeDisposable = disposables; - } - - shouldShowWelcome(): boolean { - return false; - } -} - export interface IViewPaneContainerOptions extends IPaneViewOptions { mergeViewWithContainerWhenSingleView: boolean; } @@ -860,6 +293,22 @@ class ViewPaneDropOverlay extends Themable { } } +class ViewContainerMenuActions extends CompositeMenuActions { + constructor( + viewContainer: ViewContainer, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IContextKeyService contextKeyService: IContextKeyService, + @IMenuService menuService: IMenuService, + ) { + const scopedContextKeyService = contextKeyService.createScoped(); + scopedContextKeyService.createKey('viewContainer', viewContainer.id); + const viewContainerLocationKey = scopedContextKeyService.createKey('viewContainerLocation', ViewContainerLocationToString(viewDescriptorService.getViewContainerLocation(viewContainer)!)); + super(MenuId.ViewContainerTitle, MenuId.ViewContainerTitleContext, { shouldForwardArgs: true }, scopedContextKeyService, menuService); + this._register(scopedContextKeyService); + this._register(Event.filter(viewDescriptorService.onDidChangeContainerLocation, e => e.viewContainer === viewContainer)(() => viewContainerLocationKey.set(ViewContainerLocationToString(viewDescriptorService.getViewContainerLocation(viewContainer)!)))); + } +} + export class ViewPaneContainer extends Component implements IViewPaneContainer { readonly viewContainer: ViewContainer; @@ -910,6 +359,8 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return this.paneItems.length; } + private readonly menuActions: ViewContainerMenuActions; + constructor( id: string, private options: IViewPaneContainerOptions, @@ -922,7 +373,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { @IThemeService protected themeService: IThemeService, @IStorageService protected storageService: IStorageService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, - @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService + @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService, ) { super(id, themeService, storageService); @@ -938,6 +389,9 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { this.visibleViewsCountFromCache = this.storageService.getNumber(this.visibleViewsStorageId, StorageScope.WORKSPACE, undefined); this._register(toDisposable(() => this.viewDisposables = dispose(this.viewDisposables))); this.viewContainerModel = this.viewDescriptorService.getViewContainerModel(container); + + this.menuActions = this._register(instantiationService.createInstance(ViewContainerMenuActions, container)); + this._register(this.menuActions.onDidChange(() => this.updateTitleArea())); } create(parent: HTMLElement): void { @@ -1110,57 +564,62 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { let anchor: { x: number, y: number; } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, - getActions: () => this.getContextMenuActions() + getActions: () => [...this.getContextMenuActions2()] }); } + getContextMenuActions2(): ReadonlyArray { + return this.menuActions.getContextMenuActions(); + } + getContextMenuActions(viewDescriptor?: IViewDescriptor): IAction[] { - const result: IAction[] = []; + return []; + } - let showHide = true; - if (!viewDescriptor && this.isViewMergedWithContainer()) { - viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.panes[0].id) || undefined; - showHide = false; + getActions2(): IAction[] { + const result = []; + result.push(...this.menuActions.getPrimaryActions()); + if (this.isViewMergedWithContainer()) { + result.push(...this.paneItems[0].pane.getActions()); } - - if (viewDescriptor) { - if (showHide) { - result.push({ - id: `${viewDescriptor.id}.removeView`, - label: nls.localize('hideView', "Hide"), - enabled: viewDescriptor.canToggleVisibility, - run: () => this.toggleViewVisibility(viewDescriptor!.id) - }); - } - const view = this.getView(viewDescriptor.id); - if (view) { - result.push(...view.getContextMenuActions()); - } - } - - const viewToggleActions = this.getViewsVisibilityActions(); - if (result.length && viewToggleActions.length) { - result.push(new Separator()); - } - - result.push(...viewToggleActions); - return result; } getActions(): IAction[] { - if (this.isViewMergedWithContainer()) { - return this.paneItems[0].pane.getActions(); - } - return []; } - getSecondaryActions(): IAction[] { - if (this.isViewMergedWithContainer()) { - return this.paneItems[0].pane.getSecondaryActions(); + getSecondaryActions2(): IAction[] { + const viewPaneActions = this.isViewMergedWithContainer() ? this.paneItems[0].pane.getSecondaryActions() : []; + let menuActions = this.menuActions.getSecondaryActions(); + + const viewsSubmenuActionIndex = menuActions.findIndex(action => action instanceof SubmenuItemAction && action.item.submenu === ViewsSubMenu); + if (viewsSubmenuActionIndex !== -1) { + const viewsSubmenuAction = menuActions[viewsSubmenuActionIndex]; + if (viewsSubmenuAction.actions.some(({ enabled }) => enabled)) { + if (menuActions.length === 1 && viewPaneActions.length === 0) { + menuActions = viewsSubmenuAction.actions.slice(); + } else if (viewsSubmenuActionIndex !== 0) { + menuActions = [viewsSubmenuAction, ...menuActions.slice(0, viewsSubmenuActionIndex), ...menuActions.slice(viewsSubmenuActionIndex + 1)]; + } + } else { + // Remove views submenu if none of the actions are enabled + menuActions.splice(viewsSubmenuActionIndex, 1); + } } + if (menuActions.length && viewPaneActions.length) { + return [ + ...menuActions, + new Separator(), + ...viewPaneActions + ]; + } + + return menuActions.length ? menuActions : viewPaneActions; + } + + getSecondaryActions(): IAction[] { return []; } @@ -1168,22 +627,11 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return undefined; } - getViewsVisibilityActions(): IAction[] { - return this.viewContainerModel.activeViewDescriptors.map(viewDescriptor => ({ - id: `${viewDescriptor.id}.toggleVisibility`, - label: viewDescriptor.name, - checked: this.viewContainerModel.isVisible(viewDescriptor.id), - enabled: viewDescriptor.canToggleVisibility && (!this.viewContainerModel.isVisible(viewDescriptor.id) || this.viewContainerModel.visibleViewDescriptors.length > 1), - run: () => this.toggleViewVisibility(viewDescriptor.id) - })); - } - getActionViewItem(action: IAction): IActionViewItem | undefined { if (this.isViewMergedWithContainer()) { return this.paneItems[0].pane.getActionViewItem(action); } - - return undefined; + return createActionViewItem(this.instantiationService, action); } focus(): void { @@ -1321,11 +769,11 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { this.storageService.store(this.visibleViewsStorageId, this.length, StorageScope.WORKSPACE, StorageTarget.USER); } - private onContextMenu(event: StandardMouseEvent, viewDescriptor: IViewDescriptor): void { + private onContextMenu(event: StandardMouseEvent, viewPane: ViewPane): void { event.stopPropagation(); event.preventDefault(); - const actions: IAction[] = this.getContextMenuActions(viewDescriptor); + const actions: IAction[] = viewPane.getContextMenuActions(); let anchor: { x: number, y: number } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ @@ -1364,7 +812,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const contextMenuDisposable = addDisposableListener(pane.draggableElement, 'contextmenu', e => { e.stopPropagation(); e.preventDefault(); - this.onContextMenu(new StandardMouseEvent(e), viewDescriptor); + this.onContextMenu(new StandardMouseEvent(e), pane); }); const collapseDisposable = Event.latch(Event.map(pane.onDidChange, () => !pane.isExpanded()))(collapsed => { @@ -1401,7 +849,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } - protected toggleViewVisibility(viewId: string): void { + toggleViewVisibility(viewId: string): void { // Check if view is active if (this.viewContainerModel.activeViewDescriptors.some(viewDescriptor => viewDescriptor.id === viewId)) { const visible = !this.viewContainerModel.isVisible(viewId); @@ -1641,7 +1089,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } - private isViewMergedWithContainer(): boolean { + isViewMergedWithContainer(): boolean { if (!(this.options.mergeViewWithContainerWhenSingleView && this.paneItems.length === 1)) { return false; } @@ -1665,6 +1113,21 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } +export abstract class ViewPaneContainerAction extends Action2 { + constructor(readonly desc: Readonly & { viewPaneContainerId: string }) { + super(desc); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const viewPaneContainer = accessor.get(IViewsService).getActiveViewPaneContainerWithId(this.desc.viewPaneContainerId); + if (viewPaneContainer) { + return this.runInViewPaneContainer(accessor, viewPaneContainer, ...args); + } + } + + abstract runInViewPaneContainer(accessor: ServicesAccessor, viewPaneContainer: T, ...args: any[]): any; +} + class MoveViewPosition extends Action2 { constructor(desc: Readonly, private readonly offset: number) { super(desc); diff --git a/src/vs/workbench/browser/parts/views/viewsService.ts b/src/vs/workbench/browser/parts/views/viewsService.ts index 687fbdb88..3f47ba106 100644 --- a/src/vs/workbench/browser/parts/views/viewsService.ts +++ b/src/vs/workbench/browser/parts/views/viewsService.ts @@ -196,7 +196,9 @@ export class ViewsService extends Disposable implements IViewsService { ContextKeyExpr.equals('view', viewDescriptor.id), ContextKeyExpr.equals(`${viewDescriptor.id}.defaultViewLocation`, false) ) - ) + ), + group: '1_hide', + order: 2 }], }); } @@ -392,12 +394,29 @@ export class ViewsService extends Disposable implements IViewsService { getViewProgressIndicator(viewId: string): IProgressIndicator | undefined { const viewContainer = this.viewDescriptorService.getViewContainerByViewId(viewId); - if (viewContainer === null) { + if (!viewContainer) { return undefined; } - const view = this.viewPaneContainers.get(viewContainer.id)?.viewPaneContainer?.getView(viewId); - return view?.getProgressIndicator(); + const viewPaneContainer = this.viewPaneContainers.get(viewContainer.id)?.viewPaneContainer; + if (!viewPaneContainer) { + return undefined; + } + + const view = viewPaneContainer.getView(viewId); + if (!view) { + return undefined; + } + + if (viewPaneContainer.isViewMergedWithContainer()) { + return this.getViewContainerProgressIndicator(viewContainer); + } + + return view.getProgressIndicator(); + } + + private getViewContainerProgressIndicator(viewContainer: ViewContainer): IProgressIndicator | undefined { + return this.viewDescriptorService.getViewContainerLocation(viewContainer) === ViewContainerLocation.Sidebar ? this.viewletService.getProgressIndicator(viewContainer.id) : this.panelService.getProgressIndicator(viewContainer.id); } private registerViewletOrPanel(viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation): void { @@ -451,7 +470,6 @@ export class ViewsService extends Disposable implements IViewsService { undefined, viewContainer.order, viewContainer.requestedIndex, - viewContainer.focusCommand?.id, )); } diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index 47b8f0515..d6a6f07e6 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAction } from 'vs/base/common/actions'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IViewDescriptor, IViewDescriptorService, IAddedViewDescriptorRef } from 'vs/workbench/common/views'; @@ -12,7 +11,8 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ViewPaneContainer, ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { Event } from 'vs/base/common/event'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -81,18 +81,6 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { this.getViewsForTarget(newFilterValue).forEach(item => this.viewContainerModel.setVisible(item.id, true)); } - getContextMenuActions(): IAction[] { - const result: IAction[] = Array.from(this.constantViewDescriptors.values()).map(viewDescriptor => ({ - id: `${viewDescriptor.id}.toggleVisibility`, - label: viewDescriptor.name, - checked: this.viewContainerModel.isVisible(viewDescriptor.id), - enabled: viewDescriptor.canToggleVisibility, - run: () => this.toggleViewVisibility(viewDescriptor.id) - })); - - return result; - } - private getViewsForTarget(target: string[]): IViewDescriptor[] { const views: IViewDescriptor[] = []; for (let i = 0; i < target.length; i++) { @@ -140,7 +128,4 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { abstract getTitle(): string; - getViewsVisibilityActions(): IAction[] { - return []; - } } diff --git a/src/vs/workbench/browser/style.ts b/src/vs/workbench/browser/style.ts index 0517f059b..4f1ecbcd0 100644 --- a/src/vs/workbench/browser/style.ts +++ b/src/vs/workbench/browser/style.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/style'; -import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { iconForeground, foreground, selectionBackground, focusBorder, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listHighlightForeground, inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; import { WORKBENCH_BACKGROUND, TITLE_BAR_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme'; import { isWeb, isIOS, isMacintosh, isWindows } from 'vs/base/common/platform'; @@ -13,7 +13,7 @@ import { createMetaElement } from 'vs/base/browser/dom'; import { isSafari, isStandalone } from 'vs/base/browser/browser'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { // Foreground const windowForeground = theme.getColor(foreground); diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index 01ef1665c..79ec52099 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -3,23 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; +import { Action } from 'vs/base/common/actions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { CompositeDescriptor, CompositeRegistry } from 'vs/workbench/browser/composite'; -import { IConstructorSignature0, IInstantiationService, BrandedService } from 'vs/platform/instantiation/common/instantiation'; -import { ToggleSidebarVisibilityAction, ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; +import { IConstructorSignature0, IInstantiationService, BrandedService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { URI } from 'vs/base/common/uri'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { AbstractTree } from 'vs/base/browser/ui/tree/abstractTree'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -28,6 +24,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { PaneComposite } from 'vs/workbench/browser/panecomposite'; import { Event } from 'vs/base/common/event'; import { FilterViewPaneContainer } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { Action2 } from 'vs/platform/actions/common/actions'; export abstract class Viewlet extends PaneComposite implements IViewlet { @@ -52,40 +49,6 @@ export abstract class Viewlet extends PaneComposite implements IViewlet { })); } } - - getContextMenuActions(): IAction[] { - const parentActions = [...super.getContextMenuActions()]; - if (parentActions.length) { - parentActions.push(new Separator()); - } - - const toggleSidebarPositionAction = new ToggleSidebarPositionAction(ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.getLabel(this.layoutService), this.layoutService, this.configurationService); - return [...parentActions, toggleSidebarPositionAction, - { - id: ToggleSidebarVisibilityAction.ID, - label: nls.localize('compositePart.hideSideBarLabel', "Hide Side Bar"), - enabled: true, - run: () => this.layoutService.setSideBarHidden(true) - }]; - } - - getSecondaryActions(): IAction[] { - const viewVisibilityActions = this.viewPaneContainer.getViewsVisibilityActions(); - const secondaryActions = this.viewPaneContainer.getSecondaryActions(); - if (viewVisibilityActions.length <= 1 || viewVisibilityActions.every(({ enabled }) => !enabled)) { - return secondaryActions; - } - - if (secondaryActions.length === 0) { - return viewVisibilityActions; - } - - return [ - new SubmenuAction('workbench.views', nls.localize('views', "Views"), viewVisibilityActions), - new Separator(), - ...secondaryActions - ]; - } } /** @@ -115,7 +78,7 @@ export class ViewletDescriptor extends CompositeDescriptor { requestedIndex?: number, readonly iconUrl?: URI ) { - super(ctor, id, name, cssClass, order, requestedIndex, id); + super(ctor, id, name, cssClass, order, requestedIndex); } } @@ -152,7 +115,6 @@ export class ViewletRegistry extends CompositeRegistry { getViewlets(): ViewletDescriptor[] { return this.getComposites() as ViewletDescriptor[]; } - } Registry.add(Extensions.Viewlets, new ViewletRegistry()); @@ -176,7 +138,7 @@ export class ShowViewletAction extends Action { async run(): Promise { // Pass focus to viewlet if not open or focused - if (this.otherViewletShowing() || !this.sidebarHasFocus()) { + if (otherViewletShowing(this.viewletService, this.viewletId) || !sidebarHasFocus(this.viewletService, this.layoutService)) { await this.viewletService.openViewlet(this.viewletId, true); return; } @@ -184,28 +146,42 @@ export class ShowViewletAction extends Action { // Otherwise pass focus to editor group this.editorGroupService.activeGroup.focus(); } +} - private otherViewletShowing(): boolean { - const activeViewlet = this.viewletService.getActiveViewlet(); +/** + * A reusable action to show a viewlet with a specific id. + */ +export abstract class ShowViewletAction2 extends Action2 { + /** + * Gets the viewlet ID to show. + */ + protected abstract viewletId(): string; - return !activeViewlet || activeViewlet.getId() !== this.viewletId; - } + public async run(accessor: ServicesAccessor): Promise { + const viewletService = accessor.get(IViewletService); + const editorGroupService = accessor.get(IEditorGroupsService); + const layoutService = accessor.get(IWorkbenchLayoutService); - private sidebarHasFocus(): boolean { - const activeViewlet = this.viewletService.getActiveViewlet(); - const activeElement = document.activeElement; - const sidebarPart = this.layoutService.getContainer(Parts.SIDEBAR_PART); + // Pass focus to viewlet if not open or focused + if (otherViewletShowing(viewletService, this.viewletId()) || !sidebarHasFocus(viewletService, layoutService)) { + await viewletService.openViewlet(this.viewletId(), true); + return; + } - return !!(activeViewlet && activeElement && sidebarPart && DOM.isAncestor(activeElement, sidebarPart)); + // Otherwise pass focus to editor group + editorGroupService.activeGroup.focus(); } } -export class CollapseAction extends Action { - // We need a tree getter because the action is sometimes instantiated too early - constructor(treeGetter: () => AsyncDataTree | AbstractTree, enabled: boolean, clazz?: string) { - super('workbench.action.collapse', nls.localize('collapse', "Collapse All"), clazz, enabled, async () => { - const tree = treeGetter(); - tree.collapseAll(); - }); - } -} +const otherViewletShowing = (viewletService: IViewletService, viewletId: string): boolean => { + const activeViewlet = viewletService.getActiveViewlet(); + return !activeViewlet || activeViewlet.getId() !== viewletId; +}; + +const sidebarHasFocus = (viewletService: IViewletService, layoutService: IWorkbenchLayoutService): boolean => { + const activeViewlet = viewletService.getActiveViewlet(); + const activeElement = document.activeElement; + const sidebarPart = layoutService.getContainer(Parts.SIDEBAR_PART); + + return !!(activeViewlet && activeElement && sidebarPart && DOM.isAncestor(activeElement, sidebarPart)); +}; diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index e7d260b00..78bfe33cb 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { mark } from 'vs/base/common/performance'; -import { hash } from 'vs/base/common/hash'; -import { domContentLoaded, addDisposableListener, EventType, EventHelper, detectFullscreen, addDisposableThrottledListener } from 'vs/base/browser/dom'; +import { domContentLoaded, detectFullscreen, getCookieValue } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILogService, ConsoleLogService, MultiplexLogService, getLogLevel } from 'vs/platform/log/common/log'; import { ConsoleLogInAutomationService } from 'vs/platform/log/browser/log'; @@ -24,10 +23,9 @@ import { IFileService, IFileSystemProvider } from 'vs/platform/files/common/file import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { onUnexpectedError } from 'vs/base/common/errors'; import { setFullscreen } from 'vs/base/browser/browser'; -import { isIOS, isMacintosh } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; @@ -37,12 +35,11 @@ import { SignService } from 'vs/platform/sign/browser/signService'; import type { IWorkbenchConstructionOptions, IWorkspace, IWorkbench } from 'vs/workbench/workbench.web.api'; import { BrowserStorageService } from 'vs/platform/storage/browser/storageService'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; import { FileLogService } from 'vs/platform/log/common/fileLogService'; import { toLocalISOString } from 'vs/base/common/date'; import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows'; -import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; +import { getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -61,6 +58,8 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService'; +import { BrowserWindow } from 'vs/workbench/browser/window'; +import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; class BrowserMain extends Disposable { @@ -83,35 +82,40 @@ class BrowserMain extends Disposable { const services = await this.initServices(); await domContentLoaded(); - mark('willStartWorkbench'); + mark('code/willStartWorkbench'); // Create Workbench - const workbench = new Workbench( - this.domElement, - services.serviceCollection, - services.logService - ); + const workbench = new Workbench(this.domElement, services.serviceCollection, services.logService); // Listeners this.registerListeners(workbench, services.storageService, services.logService); - // Driver - if (this.configuration.driver) { - (async () => this._register(await registerWindowDriver()))(); - } - // Startup const instantiationService = workbench.startup(); + // Window + this._register(instantiationService.createInstance(BrowserWindow)); + + // Logging + services.logService.trace('workbench configuration', JSON.stringify(this.configuration)); + // Return API Facade return instantiationService.invokeFunction(accessor => { const commandService = accessor.get(ICommandService); const lifecycleService = accessor.get(ILifecycleService); + const timerService = accessor.get(ITimerService); return { commands: { executeCommand: (command, ...args) => commandService.executeCommand(command, ...args) }, + env: { + async retrievePerformanceMarks() { + await timerService.whenReady(); + + return timerService.getPerformanceMarks(); + } + }, shutdown: () => lifecycleService.shutdown() }; }); @@ -119,46 +123,17 @@ class BrowserMain extends Disposable { private registerListeners(workbench: Workbench, storageService: BrowserStorageService, logService: ILogService): void { - // Layout - const viewport = isIOS && window.visualViewport ? window.visualViewport /** Visual viewport */ : window /** Layout viewport */; - this._register(addDisposableListener(viewport, EventType.RESIZE, () => { - logService.trace(`web.main#${isIOS && window.visualViewport ? 'visualViewport' : 'window'}Resize`); - workbench.layout(); - })); - - // Prevent the back/forward gestures in macOS - this._register(addDisposableListener(this.domElement, EventType.WHEEL, e => e.preventDefault(), { passive: false })); - - // Prevent native context menus in web - this._register(addDisposableListener(this.domElement, EventType.CONTEXT_MENU, e => EventHelper.stop(e, true))); - - // Prevent default navigation on drop - this._register(addDisposableListener(this.domElement, EventType.DROP, e => EventHelper.stop(e, true))); - // Workbench Lifecycle this._register(workbench.onBeforeShutdown(event => { if (storageService.hasPendingUpdate) { - logService.warn('Unload veto: pending storage update'); - event.veto(true); // prevent data loss from pending storage update + event.veto(true, 'veto.pendingStorageUpdate'); // prevent data loss from pending storage update } })); - this._register(workbench.onWillShutdown(() => { - storageService.close(); - })); + this._register(workbench.onWillShutdown(() => storageService.close())); this._register(workbench.onShutdown(() => this.dispose())); - - // Fullscreen (Browser) - [EventType.FULLSCREEN_CHANGE, EventType.WK_FULLSCREEN_CHANGE].forEach(event => { - this._register(addDisposableListener(document, event, () => setFullscreen(!!detectFullscreen()))); - }); - - // Fullscreen (Native) - this._register(addDisposableThrottledListener(viewport, EventType.RESIZE, () => { - setFullscreen(!!detectFullscreen()); - }, undefined, isMacintosh ? 2000 /* adjust for macOS animation */ : 800 /* can be throttled */)); } - private async initServices(): Promise<{ serviceCollection: ServiceCollection, configurationService: IConfigurationService, logService: ILogService, storageService: BrowserStorageService }> { + private async initServices(): Promise<{ serviceCollection: ServiceCollection, configurationService: IWorkbenchConfigurationService, logService: ILogService, storageService: BrowserStorageService }> { const serviceCollection = new ServiceCollection(); // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -166,7 +141,7 @@ class BrowserMain extends Disposable { // CONTRIBUTE IT VIA WORKBENCH.WEB.MAIN.TS AND registerSingleton(). // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - const payload = await this.resolveWorkspaceInitializationPayload(); + const payload = this.resolveWorkspaceInitializationPayload(); // Product const productService: IProductService = { _serviceBrand: undefined, ...product, ...this.configuration.productConfiguration }; @@ -181,9 +156,8 @@ class BrowserMain extends Disposable { const logService = new BufferLogService(getLogLevel(environmentService)); serviceCollection.set(ILogService, logService); - const connectionToken = environmentService.options.connectionToken || this.getCookieValue('vscode-tkn'); - // Remote + const connectionToken = environmentService.options.connectionToken || getCookieValue('vscode-tkn'); const remoteAuthorityResolverService = new RemoteAuthorityResolverService(connectionToken, this.configuration.resourceUriProvider); serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService); @@ -212,7 +186,7 @@ class BrowserMain extends Disposable { serviceCollection.set(IWorkspaceContextService, service); // Configuration - serviceCollection.set(IConfigurationService, service); + serviceCollection.set(IWorkbenchConfigurationService, service); return service; }), @@ -239,14 +213,16 @@ class BrowserMain extends Disposable { serviceCollection.set(IUserDataInitializationService, userDataInitializationService); if (await userDataInitializationService.requiresInitialization()) { - mark('willInitRequiredUserData'); + mark('code/willInitRequiredUserData'); + // Initialize required resources - settings & global state await userDataInitializationService.initializeRequiredResources(); // Important: Reload only local user configuration after initializing // Reloading complete configuraiton blocks workbench until remote configuration is loaded. await configurationService.reloadLocalUserConfiguration(); - mark('didInitRequiredUserData'); + + mark('code/didInitRequiredUserData'); } return { serviceCollection, configurationService, logService, storageService }; @@ -261,8 +237,9 @@ class BrowserMain extends Disposable { try { indexedDBLogProvider = await indexedDB.createFileSystemProvider(logsPath.scheme, INDEXEDDB_LOGS_OBJECT_STORE); } catch (error) { - console.error(error); + onUnexpectedError(error); } + if (indexedDBLogProvider) { fileService.registerProvider(logsPath.scheme, indexedDBLogProvider); } else { @@ -279,6 +256,7 @@ class BrowserMain extends Disposable { const connection = remoteAgentService.getConnection(); if (connection) { + // Remote file system const remoteFileSystemProvider = this._register(new RemoteFileSystemProvider(remoteAgentService)); fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); @@ -289,8 +267,9 @@ class BrowserMain extends Disposable { try { indexedDBUserDataProvider = await indexedDB.createFileSystemProvider(Schemas.userData, INDEXEDDB_USERDATA_OBJECT_STORE); } catch (error) { - console.error(error); + onUnexpectedError(error); } + fileService.registerProvider(Schemas.userData, indexedDBUserDataProvider || new InMemoryFileSystemProvider()); if (indexedDBUserDataProvider) { registerAction2(class ResetUserDataAction extends Action2 { @@ -304,21 +283,22 @@ class BrowserMain extends Disposable { } }); } + async run(accessor: ServicesAccessor): Promise { const dialogService = accessor.get(IDialogService); const hostService = accessor.get(IHostService); const result = await dialogService.confirm({ message: localize('reset user data message', "Would you like to reset your data (settings, keybindings, extensions, snippets and UI State) and reload?") }); + if (result.confirmed) { - await indexedDBUserDataProvider!.reset(); + await indexedDBUserDataProvider?.reset(); } + hostService.reload(); } }); } - - fileService.registerProvider(Schemas.userData, indexedDBUserDataProvider || new InMemoryFileSystemProvider()); } private async createStorageService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: IFileService, logService: ILogService): Promise { @@ -351,7 +331,7 @@ class BrowserMain extends Disposable { } } - private async resolveWorkspaceInitializationPayload(): Promise { + private resolveWorkspaceInitializationPayload(): IWorkspaceInitializationPayload { let workspace: IWorkspace | undefined = undefined; if (this.configuration.workspaceProvider) { workspace = this.configuration.workspaceProvider.workspace; @@ -364,18 +344,11 @@ class BrowserMain extends Disposable { // Single-folder workspace if (workspace && isFolderToOpen(workspace)) { - const id = hash(workspace.folderUri.toString()).toString(16); - return { id, folder: workspace.folderUri }; + return getSingleFolderWorkspaceIdentifier(workspace.folderUri); } return { id: 'empty-window' }; } - - private getCookieValue(name: string): string | undefined { - const match = document.cookie.match('(^|[^;]+)\\s*' + name + '\\s*=\\s*([^;]+)'); // See https://stackoverflow.com/a/25490531 - - return match ? match.pop() : undefined; - } } export function main(domElement: HTMLElement, options: IWorkbenchConstructionOptions): Promise { diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts new file mode 100644 index 000000000..337771b2d --- /dev/null +++ b/src/vs/workbench/browser/window.ts @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { setFullscreen } from 'vs/base/browser/browser'; +import { addDisposableListener, addDisposableThrottledListener, detectFullscreen, EventHelper, EventType, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { domEvent } from 'vs/base/browser/event'; +import { timeout } from 'vs/base/common/async'; +import { Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { isIOS, isMacintosh } from 'vs/base/common/platform'; +import Severity from 'vs/base/common/severity'; +import { localize } from 'vs/nls'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { BrowserLifecycleService } from 'vs/workbench/services/lifecycle/browser/lifecycleService'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +export class BrowserWindow extends Disposable { + + constructor( + @IOpenerService private readonly openerService: IOpenerService, + @ILifecycleService private readonly lifecycleService: BrowserLifecycleService, + @IDialogService private readonly dialogService: IDialogService, + @IHostService private readonly hostService: IHostService, + @ILabelService private readonly labelService: ILabelService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILogService private readonly logService: ILogService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService + ) { + super(); + + this.registerListeners(); + this.create(); + } + + private registerListeners(): void { + + // Lifecycle + this._register(this.lifecycleService.onWillShutdown(() => this.onWillShutdown())); + + // Layout + const viewport = isIOS && window.visualViewport ? window.visualViewport /** Visual viewport */ : window /** Layout viewport */; + this._register(addDisposableListener(viewport, EventType.RESIZE, () => this.onWindowResize())); + + // Prevent the back/forward gestures in macOS + this._register(addDisposableListener(this.layoutService.getWorkbenchContainer(), EventType.WHEEL, e => e.preventDefault(), { passive: false })); + + // Prevent native context menus in web + this._register(addDisposableListener(this.layoutService.getWorkbenchContainer(), EventType.CONTEXT_MENU, e => EventHelper.stop(e, true))); + + // Prevent default navigation on drop + this._register(addDisposableListener(this.layoutService.getWorkbenchContainer(), EventType.DROP, e => EventHelper.stop(e, true))); + + // Fullscreen (Browser) + [EventType.FULLSCREEN_CHANGE, EventType.WK_FULLSCREEN_CHANGE].forEach(event => { + this._register(addDisposableListener(document, event, () => setFullscreen(!!detectFullscreen()))); + }); + + // Fullscreen (Native) + this._register(addDisposableThrottledListener(viewport, EventType.RESIZE, () => { + setFullscreen(!!detectFullscreen()); + }, undefined, isMacintosh ? 2000 /* adjust for macOS animation */ : 800 /* can be throttled */)); + } + + private onWindowResize(): void { + this.logService.trace(`web.main#${isIOS && window.visualViewport ? 'visualViewport' : 'window'}Resize`); + + this.layoutService.layout(); + } + + private onWillShutdown(): void { + + // Try to detect some user interaction with the workbench + // when shutdown has happened to not show the dialog e.g. + // when navigation takes a longer time. + Event.toPromise(Event.any( + Event.once(domEvent(document.body, EventType.KEY_DOWN, true)), + Event.once(domEvent(document.body, EventType.MOUSE_DOWN, true)) + )).then(async () => { + + // Delay the dialog in case the user interacted + // with the page before it transitioned away + await timeout(3000); + + // This should normally not happen, but if for some reason + // the workbench was shutdown while the page is still there, + // inform the user that only a reload can bring back a working + // state. + const res = await this.dialogService.show( + Severity.Error, + localize('shutdownError', "An unexpected error occurred that requires a reload of this page."), + [ + localize('reload', "Reload") + ], + { + detail: localize('shutdownErrorDetail', "The workbench was unexpectedly disposed while running.") + } + ); + + if (res.choice === 0) { + this.hostService.reload(); + } + }); + } + + private create(): void { + + // Driver + if (this.environmentService.options?.driver) { + (async () => this._register(await registerWindowDriver()))(); + } + + // Handle open calls + this.setupOpenHandlers(); + + // Label formatting + this.registerLabelFormatters(); + } + + private setupOpenHandlers(): void { + + // We need to ignore the `beforeunload` event while + // we handle external links to open specifically for + // the case of application protocols that e.g. invoke + // vscode itself. We do not want to open these links + // in a new window because that would leave a blank + // window to the user, but using `window.location.href` + // will trigger the `beforeunload`. + this.openerService.setDefaultExternalOpener({ + openExternal: async (href: string) => { + if (matchesScheme(href, Schemas.http) || matchesScheme(href, Schemas.https)) { + windowOpenNoOpener(href); + } else { + this.lifecycleService.withExpectedUnload(() => window.location.href = href); + } + + return true; + } + }); + } + + private registerLabelFormatters() { + this.labelService.registerFormatter({ + scheme: Schemas.userData, + priority: true, + formatting: { + label: '${scheme}:${path}', + separator: '/', + } + }); + } +} diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index fdc99470b..03297afe7 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -33,14 +33,29 @@ import { isStandalone } from 'vs/base/browser/browser'; 'description': nls.localize('showEditorTabs', "Controls whether opened editors should show in tabs or not."), 'default': true }, + 'workbench.editor.wrapTabs': { + 'type': 'boolean', + 'markdownDescription': nls.localize('wrapTabs', "Controls whether tabs should be wrapped over multiple lines when exceeding available space or whether a scrollbar should appear instead. This value is ignored when `#workbench.editor.showTabs#` is disabled."), + 'default': false + }, 'workbench.editor.scrollToSwitchTabs': { 'type': 'boolean', - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'scrollToSwitchTabs' }, "Controls whether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behaviour for that duration. This value is ignored when `#workbench.editor.showTabs#` is `false`."), + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'scrollToSwitchTabs' }, "Controls whether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behaviour for that duration. This value is ignored when `#workbench.editor.showTabs#` is disabled."), 'default': false }, 'workbench.editor.highlightModifiedTabs': { 'type': 'boolean', - 'markdownDescription': nls.localize('highlightModifiedTabs', "Controls whether a top border is drawn on modified (dirty) editor tabs or not. This value is ignored when `#workbench.editor.showTabs#` is `false`."), + 'markdownDescription': nls.localize('highlightModifiedTabs', "Controls whether a top border is drawn on modified (dirty) editor tabs or not. This value is ignored when `#workbench.editor.showTabs#` is disabled."), + 'default': false + }, + 'workbench.editor.decorations.badges': { + 'type': 'boolean', + 'markdownDescription': nls.localize('decorations.badges', "Controls whether editor file decorations should use badges."), + 'default': false + }, + 'workbench.editor.decorations.colors': { + 'type': 'boolean', + 'markdownDescription': nls.localize('decorations.colors', "Controls whether editor file decorations should use colors."), 'default': false }, 'workbench.editor.labelFormat': { @@ -75,7 +90,7 @@ import { isStandalone } from 'vs/base/browser/browser'; 'type': 'string', 'enum': ['left', 'right', 'off'], 'default': 'right', - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'editorTabCloseButton' }, "Controls the position of the editor's tabs close buttons, or disables them when set to 'off'. This value is ignored when `#workbench.editor.showTabs#` is `false`.") + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'editorTabCloseButton' }, "Controls the position of the editor's tabs close buttons, or disables them when set to 'off'. This value is ignored when `#workbench.editor.showTabs#` is disabled.") }, 'workbench.editor.tabSizing': { 'type': 'string', @@ -85,7 +100,7 @@ import { isStandalone } from 'vs/base/browser/browser'; nls.localize('workbench.editor.tabSizing.fit', "Always keep tabs large enough to show the full editor label."), nls.localize('workbench.editor.tabSizing.shrink', "Allow tabs to get smaller when the available space is not enough to show all tabs at once.") ], - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'tabSizing' }, "Controls the sizing of editor tabs. This value is ignored when `#workbench.editor.showTabs#` is `false`.") + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'tabSizing' }, "Controls the sizing of editor tabs. This value is ignored when `#workbench.editor.showTabs#` is disabled.") }, 'workbench.editor.pinnedTabSizing': { 'type': 'string', @@ -96,7 +111,7 @@ import { isStandalone } from 'vs/base/browser/browser'; nls.localize('workbench.editor.pinnedTabSizing.compact', "A pinned tab will show in a compact form with only icon or first letter of the editor name."), nls.localize('workbench.editor.pinnedTabSizing.shrink', "A pinned tab shrinks to a compact fixed size showing parts of the editor name.") ], - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'pinnedTabSizing' }, "Controls the sizing of pinned editor tabs. Pinned tabs are sorted to the beginning of all opened tabs and typically do not close until unpinned. This value is ignored when `#workbench.editor.showTabs#` is `false`.") + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'pinnedTabSizing' }, "Controls the sizing of pinned editor tabs. Pinned tabs are sorted to the beginning of all opened tabs and typically do not close until unpinned. This value is ignored when `#workbench.editor.showTabs#` is disabled.") }, 'workbench.editor.splitSizing': { 'type': 'string', @@ -130,7 +145,12 @@ import { isStandalone } from 'vs/base/browser/browser'; }, 'workbench.editor.enablePreviewFromQuickOpen': { 'type': 'boolean', - 'description': nls.localize('enablePreviewFromQuickOpen', "Controls whether editors opened from Quick Open show as preview. Preview editors do not keep open and are reused until explicitly set to be kept open (e.g. via double click or editing)."), + 'markdownDescription': nls.localize('enablePreviewFromQuickOpen', "Controls whether editors opened from Quick Open show as preview. Preview editors do not keep open and are reused until explicitly set to be kept open (e.g. via double click or editing). This value is ignored when `#workbench.editor.enablePreview#` is disabled."), + 'default': false + }, + 'workbench.editor.enablePreviewFromCodeNavigation': { + 'type': 'boolean', + 'markdownDescription': nls.localize('enablePreviewFromCodeNavigation', "Controls whether editors remain in preview when a code navigation is started from them. Preview editors do not keep open and are reused until explicitly set to be kept open (e.g. via double click or editing). This value is ignored when `#workbench.editor.enablePreview#` is disabled."), 'default': false }, 'workbench.editor.closeOnFileDelete': { @@ -315,8 +335,8 @@ import { isStandalone } from 'vs/base/browser/browser'; nls.localize('activeFolderLong', "`\${activeFolderLong}`: the full path of the folder the file is contained in (e.g. /Users/Development/myFolder/myFileFolder)."), nls.localize('folderName', "`\${folderName}`: name of the workspace folder the file is contained in (e.g. myFolder)."), nls.localize('folderPath', "`\${folderPath}`: file path of the workspace folder the file is contained in (e.g. /Users/Development/myFolder)."), - nls.localize('rootName', "`\${rootName}`: name of the workspace (e.g. myFolder or myWorkspace)."), - nls.localize('rootPath', "`\${rootPath}`: file path of the workspace (e.g. /Users/Development/myWorkspace)."), + nls.localize('rootName', "`\${rootName}`: name of the opened workspace or folder (e.g. myFolder or myWorkspace)."), + nls.localize('rootPath', "`\${rootPath}`: file path of the opened workspace or folder (e.g. /Users/Development/myWorkspace)."), nls.localize('appName', "`\${appName}`: e.g. VS Code."), nls.localize('remoteName', "`\${remoteName}`: e.g. SSH"), nls.localize('dirty', "`\${dirty}`: a dirty indicator if the active editor is dirty."), @@ -353,7 +373,7 @@ import { isStandalone } from 'vs/base/browser/browser'; 'window.menuBarVisibility': { 'type': 'string', 'enum': ['default', 'visible', 'toggle', 'hidden', 'compact'], - 'enumDescriptions': [ + 'markdownEnumDescriptions': [ nls.localize('window.menuBarVisibility.default', "Menu is only hidden in full screen mode."), nls.localize('window.menuBarVisibility.visible', "Menu is always visible even in full screen mode."), nls.localize('window.menuBarVisibility.toggle', "Menu is hidden but can be displayed via Alt key."), @@ -463,7 +483,7 @@ import { isStandalone } from 'vs/base/browser/browser'; }, 'zenMode.restore': { 'type': 'boolean', - 'default': false, + 'default': true, 'description': nls.localize('zenMode.restore', "Controls whether a window should restore to zen mode if it was exited in zen mode.") }, 'zenMode.silentNotifications': { diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 2172c0944..aedb2a6f6 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -8,7 +8,7 @@ import 'vs/workbench/browser/style'; import { localize } from 'vs/nls'; import { Emitter, setGlobalLeakWarningThreshold } from 'vs/base/common/event'; import { runWhenIdle } from 'vs/base/common/async'; -import { getZoomLevel, isFirefox, isSafari, isChrome } from 'vs/base/browser/browser'; +import { getZoomLevel, isFirefox, isSafari, isChrome, getPixelRatio } from 'vs/base/browser/browser'; import { mark } from 'vs/base/common/performance'; import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -177,10 +177,17 @@ export class Workbench extends Layout { // Layout Service serviceCollection.set(IWorkbenchLayoutService, this); - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - // NOTE: DO NOT ADD ANY OTHER SERVICE INTO THE COLLECTION HERE. - // CONTRIBUTE IT VIA WORKBENCH.DESKTOP.MAIN.TS AND registerSingleton(). - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // + // NOTE: Please do NOT register services here. Use `registerSingleton()` + // from `workbench.common.main.ts` if the service is shared between + // native and web or `workbench.sandbox.main.ts` if the service + // is native only. + // + // DO NOT add services to `workbench.desktop.main.ts`, always add + // to `workbench.sandbox.main.ts` to support our Electron sandbox + // + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // All Contributed Services const contributedServices = getSingletonServiceDescriptors(); @@ -284,7 +291,7 @@ export class Workbench extends Layout { } } - readFontInfo(BareFontInfo.createFromRawSettings(configurationService.getValue('editor'), getZoomLevel())); + readFontInfo(BareFontInfo.createFromRawSettings(configurationService.getValue('editor'), getZoomLevel(), getPixelRatio())); } private storeFontInfo(storageService: IStorageService): void { @@ -412,10 +419,10 @@ export class Workbench extends Layout { }, 2500); // Telemetry: startup metrics - mark('didStartWorkbench'); + mark('code/didStartWorkbench'); // Perf reporting (devtools) - performance.measure('perf: workbench create & restore', 'didLoadWorkbenchMain', 'didStartWorkbench'); + performance.measure('perf: workbench create & restore', 'code/didLoadWorkbenchMain', 'code/didStartWorkbench'); } } } diff --git a/src/vs/workbench/common/actions.ts b/src/vs/workbench/common/actions.ts index de82ce88e..dfe61debb 100644 --- a/src/vs/workbench/common/actions.ts +++ b/src/vs/workbench/common/actions.ts @@ -128,5 +128,6 @@ Registry.add(Extensions.WorkbenchActions, new class implements IWorkbenchActionR export const CATEGORIES = { View: { value: localize('view', "View"), original: 'View' }, Help: { value: localize('help', "Help"), original: 'Help' }, + Preferences: { value: localize('preferences', "Preferences"), original: 'Preferences' }, Developer: { value: localize({ key: 'developer', comment: ['A developer on Code itself or someone diagnosing issues in Code'] }, "Developer"), original: 'Developer' } }; diff --git a/src/vs/workbench/common/composite.ts b/src/vs/workbench/common/composite.ts index 8b48a7f4d..d27f59b36 100644 --- a/src/vs/workbench/common/composite.ts +++ b/src/vs/workbench/common/composite.ts @@ -18,6 +18,11 @@ export interface IComposite { */ readonly onDidBlur: Event; + /** + * Returns true if the composite has focus. + */ + hasFocus(): boolean; + /** * Returns the unique identifier of this composite. */ diff --git a/src/vs/workbench/common/dialogs.ts b/src/vs/workbench/common/dialogs.ts index 5de8eac68..5e0561583 100644 --- a/src/vs/workbench/common/dialogs.ts +++ b/src/vs/workbench/common/dialogs.ts @@ -18,6 +18,7 @@ export interface IDialogHandle { } export interface IDialogsModel { + readonly onDidShowDialog: Event; readonly dialogs: IDialogViewItem[]; @@ -26,6 +27,7 @@ export interface IDialogsModel { } export class DialogsModel extends Disposable implements IDialogsModel { + readonly dialogs: IDialogViewItem[] = []; private readonly _onDidShowDialog = this._register(new Emitter()); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index f104149d8..883328209 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -74,7 +74,7 @@ export interface IEditorPane extends IComposite { /** * The assigned options of the editor. */ - readonly options: EditorOptions | undefined; + readonly options: IEditorOptions | undefined; /** * The assigned group this editor is showing in. @@ -1215,7 +1215,7 @@ export interface IEditorOpenContext { * An editor is new for a group if it was not part of the group before and * otherwise was already opened in the group and just became the active editor. * - * This hint can e.g. be used to decide wether to restore view state or not. + * This hint can e.g. be used to decide whether to restore view state or not. */ newInGroup?: boolean; } @@ -1257,14 +1257,15 @@ export interface IEditorCloseEvent extends IEditorIdentifier { export type GroupIdentifier = number; export interface IWorkbenchEditorConfiguration { - workbench: { - editor: IEditorPartConfiguration, - iconTheme: string; + workbench?: { + editor?: IEditorPartConfiguration, + iconTheme?: string; }; } interface IEditorPartConfiguration { showTabs?: boolean; + wrapTabs?: boolean; scrollToSwitchTabs?: boolean; highlightModifiedTabs?: boolean; tabCloseButton?: 'left' | 'right' | 'off'; @@ -1275,6 +1276,7 @@ interface IEditorPartConfiguration { showIcons?: boolean; enablePreview?: boolean; enablePreviewFromQuickOpen?: boolean; + enablePreviewFromCodeNavigation?: boolean; closeOnFileDelete?: boolean; openPositioning?: 'left' | 'right' | 'first' | 'last'; openSideBySideDirection?: 'right' | 'down'; @@ -1290,6 +1292,10 @@ interface IEditorPartConfiguration { value?: number; perEditorGroup?: boolean; }; + decorations?: { + badges?: boolean; + colors?: boolean; + } } export interface IEditorPartOptions extends IEditorPartConfiguration { @@ -1328,7 +1334,9 @@ class EditorResourceAccessorImpl { /** * The original URI of an editor is the URI that was used originally to open * the editor and should be used whenever the URI is presented to the user, - * e.g. as a label. + * e.g. as a label together with utility methods such as `ResourceLabel` or + * `ILabelService` that can turn this original URI into the best form for + * presenting. * * In contrast, the canonical URI (#getCanonicalUri) may be different and should * be used whenever the URI is used to e.g. compare with other editors or when diff --git a/src/vs/workbench/common/editor/diffEditorInput.ts b/src/vs/workbench/common/editor/diffEditorInput.ts index 38051db4d..1b0eed959 100644 --- a/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/src/vs/workbench/common/editor/diffEditorInput.ts @@ -13,6 +13,7 @@ import { dirname } from 'vs/base/common/resources'; import { ILabelService } from 'vs/platform/label/common/label'; import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; +import { withNullAsUndefined } from 'vs/base/common/types'; /** * The base editor input for the diff editor. It is made up of two editor inputs, the original version @@ -110,21 +111,18 @@ export class DiffEditorInput extends SideBySideEditorInput { private async createModel(): Promise { // Join resolve call over two inputs and build diff editor model - const models = await Promise.all([ + const [originalEditorModel, modifiedEditorModel] = await Promise.all([ this.originalInput.resolve(), this.modifiedInput.resolve() ]); - const originalEditorModel = models[0]; - const modifiedEditorModel = models[1]; - // If both are text models, return textdiffeditor model if (modifiedEditorModel instanceof BaseTextEditorModel && originalEditorModel instanceof BaseTextEditorModel) { return new TextDiffEditorModel(originalEditorModel, modifiedEditorModel); } // Otherwise return normal diff model - return new DiffEditorModel(originalEditorModel, modifiedEditorModel); + return new DiffEditorModel(withNullAsUndefined(originalEditorModel), withNullAsUndefined(modifiedEditorModel)); } matches(otherInput: unknown): boolean { diff --git a/src/vs/workbench/common/editor/diffEditorModel.ts b/src/vs/workbench/common/editor/diffEditorModel.ts index dfa51d62d..5550bf997 100644 --- a/src/vs/workbench/common/editor/diffEditorModel.ts +++ b/src/vs/workbench/common/editor/diffEditorModel.ts @@ -12,13 +12,13 @@ import { IEditorModel } from 'vs/platform/editor/common/editor'; */ export class DiffEditorModel extends EditorModel { - protected readonly _originalModel: IEditorModel | null; - get originalModel(): IEditorModel | null { return this._originalModel; } + protected readonly _originalModel: IEditorModel | undefined; + get originalModel(): IEditorModel | undefined { return this._originalModel; } - protected readonly _modifiedModel: IEditorModel | null; - get modifiedModel(): IEditorModel | null { return this._modifiedModel; } + protected readonly _modifiedModel: IEditorModel | undefined; + get modifiedModel(): IEditorModel | undefined { return this._modifiedModel; } - constructor(originalModel: IEditorModel | null, modifiedModel: IEditorModel | null) { + constructor(originalModel: IEditorModel | undefined, modifiedModel: IEditorModel | undefined) { super(); this._originalModel = originalModel; @@ -28,7 +28,7 @@ export class DiffEditorModel extends EditorModel { async load(): Promise { await Promise.all([ this._originalModel?.load(), - this._modifiedModel?.load(), + this._modifiedModel?.load() ]); return this; diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index 8d7410ac5..d1580899a 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -724,12 +724,19 @@ export class EditorGroup extends Disposable { clone(): EditorGroup { const group = this.instantiationService.createInstance(EditorGroup, undefined); + + // Copy over group properties group.editors = this.editors.slice(0); group.mru = this.mru.slice(0); group.preview = this.preview; group.active = this.active; group.sticky = this.sticky; + // Ensure to register listeners for each editor + for (const editor of group.editors) { + group.registerEditorListeners(editor); + } + return group; } diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index a72d26526..0167182c2 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -97,7 +97,7 @@ export class ResourceEditorInput extends AbstractTextResourceEditorInput impleme ref.dispose(); this.modelReference = undefined; - throw new Error(`Unexpected model for ResourcEditorInput: ${this.resource}`); + throw new Error(`Unexpected model for ResourceEditorInput: ${this.resource}`); } this.cachedModel = model; diff --git a/src/vs/workbench/common/editor/resourceEditorModel.ts b/src/vs/workbench/common/editor/resourceEditorModel.ts index e8e58c3f9..56f7aefe2 100644 --- a/src/vs/workbench/common/editor/resourceEditorModel.ts +++ b/src/vs/workbench/common/editor/resourceEditorModel.ts @@ -23,7 +23,7 @@ export class ResourceEditorModel extends BaseTextEditorModel { dispose(): void { - // TODO@Joao: force this class to dispose the underlying model + // force this class to dispose the underlying model if (this.textEditorModelHandle) { this.modelService.destroyModel(this.textEditorModelHandle); } diff --git a/src/vs/workbench/common/editor/textDiffEditorModel.ts b/src/vs/workbench/common/editor/textDiffEditorModel.ts index fcb97b428..98528b2e5 100644 --- a/src/vs/workbench/common/editor/textDiffEditorModel.ts +++ b/src/vs/workbench/common/editor/textDiffEditorModel.ts @@ -14,14 +14,14 @@ import { DiffEditorModel } from 'vs/workbench/common/editor/diffEditorModel'; */ export class TextDiffEditorModel extends DiffEditorModel { - protected readonly _originalModel: BaseTextEditorModel | null; - get originalModel(): BaseTextEditorModel | null { return this._originalModel; } + protected readonly _originalModel: BaseTextEditorModel | undefined; + get originalModel(): BaseTextEditorModel | undefined { return this._originalModel; } - protected readonly _modifiedModel: BaseTextEditorModel | null; - get modifiedModel(): BaseTextEditorModel | null { return this._modifiedModel; } + protected readonly _modifiedModel: BaseTextEditorModel | undefined; + get modifiedModel(): BaseTextEditorModel | undefined { return this._modifiedModel; } - private _textDiffEditorModel: IDiffEditorModel | null = null; - get textDiffEditorModel(): IDiffEditorModel | null { return this._textDiffEditorModel; } + private _textDiffEditorModel: IDiffEditorModel | undefined = undefined; + get textDiffEditorModel(): IDiffEditorModel | undefined { return this._textDiffEditorModel; } constructor(originalModel: BaseTextEditorModel, modifiedModel: BaseTextEditorModel) { super(originalModel, modifiedModel); @@ -73,7 +73,7 @@ export class TextDiffEditorModel extends DiffEditorModel { // inside. We never created the two models (original and modified) so we can not dispose // them without sideeffects. Rather rely on the models getting disposed when their related // inputs get disposed from the diffEditorInput. - this._textDiffEditorModel = null; + this._textDiffEditorModel = undefined; super.dispose(); } diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index f730d008e..3225ea5a9 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -18,7 +18,7 @@ import { withUndefinedAsNull } from 'vs/base/common/types'; */ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel, IModeSupport { - protected textEditorModelHandle: URI | null = null; + protected textEditorModelHandle: URI | undefined = undefined; private createdEditorModel: boolean | undefined; @@ -52,7 +52,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel private registerModelDisposeListener(model: ITextModel): void { this.modelDisposeListener.value = model.onWillDispose(() => { - this.textEditorModelHandle = null; // make sure we do not dispose code editor model again + this.textEditorModelHandle = undefined; // make sure we do not dispose code editor model again this.dispose(); }); } @@ -178,7 +178,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel this.modelService.destroyModel(this.textEditorModelHandle); } - this.textEditorModelHandle = null; + this.textEditorModelHandle = undefined; this.createdEditorModel = false; super.dispose(); diff --git a/src/vs/workbench/common/editor/textResourceEditorInput.ts b/src/vs/workbench/common/editor/textResourceEditorInput.ts index e55b55679..b738dca4c 100644 --- a/src/vs/workbench/common/editor/textResourceEditorInput.ts +++ b/src/vs/workbench/common/editor/textResourceEditorInput.ts @@ -199,14 +199,14 @@ export abstract class AbstractTextResourceEditorInput extends EditorInput implem } // Normal save - return this.doSave(group, options, false); + return this.doSave(options, false); } saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { - return this.doSave(group, options, true); + return this.doSave(options, true); } - private async doSave(group: GroupIdentifier, options: ISaveOptions | undefined, saveAs: boolean): Promise { + private async doSave(options: ISaveOptions | undefined, saveAs: boolean): Promise { // Save / Save As let target: URI | undefined; @@ -220,8 +220,13 @@ export abstract class AbstractTextResourceEditorInput extends EditorInput implem return undefined; // save cancelled } - // If the target is a different resource, return with a new editor input - if (!isEqual(target, this.preferredResource)) { + // If this save operation results in a new editor, either + // because it was saved to disk (e.g. from untitled) or + // through an explicit "Save As", make sure to replace it. + if ( + target.scheme !== this.resource.scheme || + (saveAs && !isEqual(target, this.preferredResource)) + ) { return this.editorService.createEditorInput({ resource: target }); } diff --git a/src/vs/workbench/common/panel.ts b/src/vs/workbench/common/panel.ts index 7b836be95..555e85dcf 100644 --- a/src/vs/workbench/common/panel.ts +++ b/src/vs/workbench/common/panel.ts @@ -9,5 +9,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const ActivePanelContext = new RawContextKey('activePanel', ''); export const PanelFocusContext = new RawContextKey('panelFocus', false); export const PanelPositionContext = new RawContextKey('panelPosition', 'bottom'); +export const PanelVisibleContext = new RawContextKey('panelVisible', false); +export const PanelMaximizedContext = new RawContextKey('panelMaximized', false); export interface IPanel extends IComposite { } diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 53f29efbd..317cfd2fc 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -339,7 +339,7 @@ export const STATUS_BAR_FOREGROUND = registerColor('statusBar.foreground', { dark: '#FFFFFF', light: '#FFFFFF', hc: '#FFFFFF' -}, nls.localize('statusBarForeground', "Status bar foreground color when a workspace is opened. The status bar is shown in the bottom of the window.")); +}, nls.localize('statusBarForeground', "Status bar foreground color when a workspace or folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_NO_FOLDER_FOREGROUND = registerColor('statusBar.noFolderForeground', { dark: STATUS_BAR_FOREGROUND, @@ -351,7 +351,7 @@ export const STATUS_BAR_BACKGROUND = registerColor('statusBar.background', { dark: '#007ACC', light: '#007ACC', hc: null -}, nls.localize('statusBarBackground', "Status bar background color when a workspace is opened. The status bar is shown in the bottom of the window.")); +}, nls.localize('statusBarBackground', "Status bar background color when a workspace or folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_NO_FOLDER_BACKGROUND = registerColor('statusBar.noFolderBackground', { dark: '#68217A', diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 0744cecf6..7bbafb802 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -27,9 +27,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { mixin } from 'vs/base/common/objects'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; - -export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test'; -export const testViewIcon = registerIcon('test-view-icon', Codicon.beaker, localize('testViewIcon', 'View icon of the test view.')); +import { CancellationToken } from 'vs/base/common/cancellation'; export const defaultViewIcon = registerIcon('default-view-icon', Codicon.window, localize('defaultViewIcon', 'Default view icon.')); @@ -38,11 +36,18 @@ export namespace Extensions { export const ViewsRegistry = 'workbench.registry.view'; } -export enum ViewContainerLocation { +export const enum ViewContainerLocation { Sidebar, Panel } +export function ViewContainerLocationToString(viewContainerLocation: ViewContainerLocation) { + switch (viewContainerLocation) { + case ViewContainerLocation.Sidebar: return 'sidebar'; + case ViewContainerLocation.Panel: return 'panel'; + } +} + export interface IViewContainerDescriptor { readonly id: string; @@ -256,6 +261,8 @@ export interface IAddedViewDescriptorState { export interface IViewContainerModel { + readonly viewContainer: ViewContainer; + readonly title: string; readonly icon: ThemeIcon | URI | undefined; readonly onDidChangeContainerInfo: Event<{ title?: boolean, icon?: boolean }>; @@ -505,7 +512,7 @@ export interface IViewsService { * View Contexts */ export const FocusedViewContext = new RawContextKey('focusedView', ''); -export function getVisbileViewContextKey(viewId: string): string { return `${viewId}.visible`; } +export function getVisbileViewContextKey(viewId: string): string { return `view.${viewId}.visible`; } export const IViewDescriptorService = createDecorator('viewDescriptorService'); @@ -684,21 +691,24 @@ export class ResolvableTreeItem implements ITreeItem { command?: Command; children?: ITreeItem[]; accessibilityInformation?: IAccessibilityInformation; - resolve: () => Promise; + resolve: (token: CancellationToken) => Promise; private resolved: boolean = false; private _hasResolve: boolean = false; - constructor(treeItem: ITreeItem, resolve?: (() => Promise)) { + constructor(treeItem: ITreeItem, resolve?: ((token: CancellationToken) => Promise)) { mixin(this, treeItem); this._hasResolve = !!resolve; - this.resolve = async () => { + this.resolve = async (token: CancellationToken) => { if (resolve && !this.resolved) { - const resolvedItem = await resolve(); + const resolvedItem = await resolve(token); if (resolvedItem) { - // Resolvable elements. Currently only tooltip. - this.tooltip = resolvedItem.tooltip; + // Resolvable elements. Currently tooltip and command. + this.tooltip = this.tooltip ?? resolvedItem.tooltip; + this.command = this.command ?? resolvedItem.command; } } - this.resolved = true; + if (!token.isCancellationRequested) { + this.resolved = true; + } }; } get hasResolve(): boolean { @@ -756,5 +766,6 @@ export interface IViewPaneContainer { getActionViewItem(action: IAction): IActionViewItem | undefined; getActionsContext(): unknown; getView(viewId: string): IView | undefined; + toggleViewVisibility(viewId: string): void; saveState(): void; } diff --git a/src/vs/workbench/contrib/backup/common/backupTracker.ts b/src/vs/workbench/contrib/backup/common/backupTracker.ts index d8f35fc9d..f9767e534 100644 --- a/src/vs/workbench/contrib/backup/common/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/common/backupTracker.ts @@ -18,7 +18,7 @@ export abstract class BackupTracker extends Disposable { private readonly mapWorkingCopyToContentVersion = new Map(); // A map of scheduled pending backups for working copies - private readonly pendingBackups = new Map(); + protected readonly pendingBackups = new Map(); constructor( protected readonly backupFileService: IBackupFileService, @@ -43,7 +43,7 @@ export abstract class BackupTracker extends Disposable { this._register(this.workingCopyService.onDidChangeContent(workingCopy => this.onDidChangeContent(workingCopy))); // Lifecycle (handled in subclasses) - this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason))); + this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason), 'veto.backups')); } private onDidRegister(workingCopy: IWorkingCopy): void { diff --git a/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts b/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts index d2c4f92fb..c4a4e33a5 100644 --- a/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts @@ -9,7 +9,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILifecycleService, LifecyclePhase, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { ConfirmResult, IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ConfirmResult, IFileDialogService, IDialogService, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { isMacintosh } from 'vs/base/common/platform'; @@ -20,7 +20,9 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { SaveReason } from 'vs/workbench/common/editor'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { raceCancellation } from 'vs/base/common/async'; export class NativeBackupTracker extends BackupTracker implements IWorkbenchContribution { @@ -47,7 +49,8 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont @INativeHostService private readonly nativeHostService: INativeHostService, @ILogService logService: ILogService, @IEditorService private readonly editorService: IEditorService, - @IEnvironmentService private readonly environmentService: IEnvironmentService + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IProgressService private readonly progressService: IProgressService ) { super(backupFileService, workingCopyService, logService, lifecycleService); } @@ -121,7 +124,7 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont // we ran a backup but received an error that we show to the user if (backupError) { - this.showErrorDialog(localize('backupTrackerBackupFailed', "One or more dirty editors could not be saved to the back up location."), backupError); + this.showErrorDialog(localize('backupTrackerBackupFailed', "The following dirty editors could not be saved to the back up location."), workingCopies, backupError); return true; // veto (the backup failed) } @@ -131,14 +134,21 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont try { return await this.confirmBeforeShutdown(workingCopies.filter(workingCopy => !backups.includes(workingCopy))); } catch (error) { - this.showErrorDialog(localize('backupTrackerConfirmFailed', "One or more dirty editors could not be saved or reverted."), error); + this.showErrorDialog(localize('backupTrackerConfirmFailed', "The following dirty editors could not be saved or reverted."), workingCopies, error); return true; // veto (save or revert failed) } } - private showErrorDialog(msg: string, error?: Error): void { - this.dialogService.show(Severity.Error, msg, [localize('ok', 'OK')], { detail: localize('backupErrorDetails', "Try saving or reverting the dirty editors first and then try again.") }); + private showErrorDialog(msg: string, workingCopies: readonly IWorkingCopy[], error?: Error): void { + const dirtyEditors = workingCopies.filter(workingCopy => workingCopy.isDirty()); + + const advice = localize('backupErrorDetails', "Try saving or reverting the dirty editors first and then try again."); + const detail = dirtyEditors.length + ? getFileNamesMessage(dirtyEditors.map(x => x.name)) + '\n' + advice + : advice; + + this.dialogService.show(Severity.Error, msg, [localize('ok', 'OK')], { detail }); this.logService.error(error ? `[backup tracker] ${msg}: ${error}` : `[backup tracker] ${msg}`); } @@ -234,9 +244,9 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont return true; // veto (user canceled) } - private async doSaveAllBeforeShutdown(workingCopies: IWorkingCopy[], reason: SaveReason): Promise; - private async doSaveAllBeforeShutdown(includeUntitled: boolean, reason: SaveReason): Promise; - private async doSaveAllBeforeShutdown(arg1: IWorkingCopy[] | boolean, reason: SaveReason): Promise { + private doSaveAllBeforeShutdown(workingCopies: IWorkingCopy[], reason: SaveReason): Promise; + private doSaveAllBeforeShutdown(includeUntitled: boolean, reason: SaveReason): Promise; + private doSaveAllBeforeShutdown(arg1: IWorkingCopy[] | boolean, reason: SaveReason): Promise { const workingCopies = Array.isArray(arg1) ? arg1 : this.workingCopyService.dirtyWorkingCopies.filter(workingCopy => { if (arg1 === false && (workingCopy.capabilities & WorkingCopyCapabilities.Untitled)) { return false; // skip untitled unless explicitly included @@ -245,21 +255,34 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont return true; }); - // Skip save participants on shutdown for performance reasons - const saveOptions = { skipSaveParticipants: true, reason }; + const cts = new CancellationTokenSource(); + return this.progressService.withProgress({ + location: ProgressLocation.Notification, + cancellable: true, // for https://github.com/microsoft/vscode/issues/112278 + delay: 800, // delay notification so that it only appears when saving takes a long time + title: localize('saveBeforeShutdown', "Waiting for dirty editors to save...") + }, () => { + const saveAllPromise = (async () => { - // First save through the editor service if we save all to benefit - // from some extras like switching to untitled dirty editors before saving. - let result: boolean | undefined = undefined; - if (typeof arg1 === 'boolean' || workingCopies.length === this.workingCopyService.dirtyCount) { - result = await this.editorService.saveAll({ includeUntitled: typeof arg1 === 'boolean' ? arg1 : true, ...saveOptions }); - } + // Skip save participants on shutdown for performance reasons + const saveOptions = { skipSaveParticipants: true, reason }; - // If we still have dirty working copies, save those directly - // unless the save was not successful (e.g. cancelled) - if (result !== false) { - await Promise.all(workingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.save(saveOptions) : true)); - } + // First save through the editor service if we save all to benefit + // from some extras like switching to untitled dirty editors before saving. + let result: boolean | undefined = undefined; + if (typeof arg1 === 'boolean' || workingCopies.length === this.workingCopyService.dirtyCount) { + result = await this.editorService.saveAll({ includeUntitled: typeof arg1 === 'boolean' ? arg1 : true, ...saveOptions }); + } + + // If we still have dirty working copies, save those directly + // unless the save was not successful (e.g. cancelled) + if (result !== false) { + await Promise.all(workingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.save(saveOptions) : true)); + } + })(); + + return raceCancellation(saveAllPromise, cts.token); + }, () => cts.dispose(true)); } private async doRevertAllBeforeShutdown(workingCopies: IWorkingCopy[]): Promise { diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts b/src/vs/workbench/contrib/backup/test/browser/backupRestorer.test.ts similarity index 56% rename from src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts rename to src/vs/workbench/contrib/backup/test/browser/backupRestorer.test.ts index dfe8325aa..1835bff59 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts +++ b/src/vs/workbench/contrib/backup/test/browser/backupRestorer.test.ts @@ -4,46 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as platform from 'vs/base/common/platform'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; -import * as pfs from 'vs/base/node/pfs'; +import { isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { DefaultEndOfLine } from 'vs/editor/common/model'; -import { hashPath } from 'vs/workbench/services/backup/electron-browser/backupFileService'; -import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-sandbox/backupTracker'; -import { workbenchInstantiationService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; -import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorInput } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/electron-browser/backupFileService.test'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; -import { TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; +import { InMemoryTestBackupFileService, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { BackupRestorer } from 'vs/workbench/contrib/backup/common/backupRestorer'; - -const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); -const backupHome = path.join(userdataDir, 'Backups'); -const workspacesJsonPath = path.join(backupHome, 'workspaces.json'); - -const workspaceResource = URI.file(platform.isWindows ? 'c:\\workspace' : '/workspace'); -const workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource)); -const fooFile = URI.file(platform.isWindows ? 'c:\\Foo' : '/Foo'); -const barFile = URI.file(platform.isWindows ? 'c:\\Bar' : '/Bar'); -const untitledFile1 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); -const untitledFile2 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-2' }); +import { BrowserBackupTracker } from 'vs/workbench/contrib/backup/browser/backupTracker'; class TestBackupRestorer extends BackupRestorer { async doRestoreBackups(): Promise { @@ -54,38 +28,13 @@ class TestBackupRestorer extends BackupRestorer { suite('BackupRestorer', () => { let accessor: TestServiceAccessor; - let disposables: IDisposable[] = []; - - setup(async () => { - disposables.push(Registry.as(EditorExtensions.Editors).registerEditor( - EditorDescriptor.create( - TextFileEditor, - TextFileEditor.ID, - 'Text File Editor' - ), - [new SyncDescriptor(FileEditorInput)] - )); - - // Delete any existing backups completely and then re-create it. - await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); - await pfs.mkdirp(backupHome); - - return pfs.writeFile(workspacesJsonPath, ''); - }); - - teardown(async () => { - dispose(disposables); - disposables = []; - - (accessor.textFileService.files).dispose(); - - return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); - }); + const fooFile = URI.file(isWindows ? 'c:\\Foo' : '/Foo'); + const barFile = URI.file(isWindows ? 'c:\\Bar' : '/Bar'); + const untitledFile1 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); + const untitledFile2 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-2' }); test('Restore backups', async function () { - this.timeout(20000); - - const backupFileService = new NodeTestBackupFileService(workspaceBackupPath); + const backupFileService = new InMemoryTestBackupFileService(); const instantiationService = workbenchInstantiationService(); instantiationService.stub(IBackupFileService, backupFileService); @@ -102,18 +51,18 @@ suite('BackupRestorer', () => { await part.whenRestored; - const tracker = instantiationService.createInstance(NativeBackupTracker); + const tracker = instantiationService.createInstance(BrowserBackupTracker); const restorer = instantiationService.createInstance(TestBackupRestorer); // Backup 2 normal files and 2 untitled file - await backupFileService.backup(untitledFile1, createTextBufferFactory('untitled-1').create(DefaultEndOfLine.LF).createSnapshot(false)); - await backupFileService.backup(untitledFile2, createTextBufferFactory('untitled-2').create(DefaultEndOfLine.LF).createSnapshot(false)); - await backupFileService.backup(fooFile, createTextBufferFactory('fooFile').create(DefaultEndOfLine.LF).createSnapshot(false)); - await backupFileService.backup(barFile, createTextBufferFactory('barFile').create(DefaultEndOfLine.LF).createSnapshot(false)); + await backupFileService.backup(untitledFile1, createTextBufferFactory('untitled-1').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + await backupFileService.backup(untitledFile2, createTextBufferFactory('untitled-2').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + await backupFileService.backup(fooFile, createTextBufferFactory('fooFile').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + await backupFileService.backup(barFile, createTextBufferFactory('barFile').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); // Verify backups restored and opened as dirty await restorer.doRestoreBackups(); - assert.equal(editorService.count, 4); + assert.strictEqual(editorService.count, 4); assert.ok(editorService.editors.every(editor => editor.isDirty())); let counter = 0; @@ -152,7 +101,7 @@ suite('BackupRestorer', () => { } } - assert.equal(counter, 4); + assert.strictEqual(counter, 4); part.dispose(); tracker.dispose(); diff --git a/src/vs/workbench/contrib/backup/test/browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/browser/backupTracker.test.ts new file mode 100644 index 000000000..a75a319c7 --- /dev/null +++ b/src/vs/workbench/contrib/backup/test/browser/backupTracker.test.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; +import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { toResource } from 'vs/base/test/common/utils'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IWorkingCopyBackup, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { InMemoryTestBackupFileService, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { timeout } from 'vs/base/common/async'; +import { BrowserBackupTracker } from 'vs/workbench/contrib/backup/browser/backupTracker'; + +class TestBackupTracker extends BrowserBackupTracker { + + constructor( + @IBackupFileService backupFileService: IBackupFileService, + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @ILifecycleService lifecycleService: ILifecycleService, + @ILogService logService: ILogService, + ) { + super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, logService); + } + + protected getBackupScheduleDelay(): number { + return 10; // Reduce timeout for tests + } +} + +suite('BackupTracker (browser)', function () { + let accessor: TestServiceAccessor; + + async function createTracker(): Promise<{ accessor: TestServiceAccessor, part: EditorPart, tracker: BackupTracker, backupFileService: InMemoryTestBackupFileService, instantiationService: IInstantiationService, cleanup: () => void }> { + const backupFileService = new InMemoryTestBackupFileService(); + const instantiationService = workbenchInstantiationService(); + instantiationService.stub(IBackupFileService, backupFileService); + + const part = instantiationService.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + instantiationService.stub(IEditorGroupsService, part); + + const editorService: EditorService = instantiationService.createInstance(EditorService); + instantiationService.stub(IEditorService, editorService); + + accessor = instantiationService.createInstance(TestServiceAccessor); + + await part.whenRestored; + + const tracker = instantiationService.createInstance(TestBackupTracker); + + const cleanup = () => { + part.dispose(); + tracker.dispose(); + }; + + return { accessor, part, tracker, backupFileService, instantiationService, cleanup }; + } + + async function untitledBackupTest(untitled: IUntitledTextResourceEditorInput = {}): Promise { + const { accessor, cleanup, backupFileService } = await createTracker(); + + const untitledEditor = (await accessor.editorService.openEditor(untitled))?.input as UntitledTextEditorInput; + + const untitledModel = await untitledEditor.resolve(); + + if (!untitled?.contents) { + untitledModel.textEditorModel.setValue('Super Good'); + } + + await backupFileService.joinBackupResource(); + + assert.strictEqual(backupFileService.hasBackupSync(untitledEditor.resource), true); + + untitledModel.dispose(); + + await backupFileService.joinDiscardBackup(); + + assert.strictEqual(backupFileService.hasBackupSync(untitledEditor.resource), false); + + cleanup(); + } + + test('Track backups (untitled)', function () { + return untitledBackupTest(); + }); + + test('Track backups (untitled with initial contents)', function () { + return untitledBackupTest({ contents: 'Foo Bar' }); + }); + + test('Track backups (custom)', async function () { + const { accessor, cleanup, backupFileService } = await createTracker(); + + class TestBackupWorkingCopy extends TestWorkingCopy { + + backupDelay = 0; + + constructor(resource: URI) { + super(resource); + + accessor.workingCopyService.registerWorkingCopy(this); + } + + async backup(token: CancellationToken): Promise { + await timeout(this.backupDelay); + + return {}; + } + } + + const resource = toResource.call(this, '/path/custom.txt'); + const customWorkingCopy = new TestBackupWorkingCopy(resource); + + // Normal + customWorkingCopy.setDirty(true); + await backupFileService.joinBackupResource(); + assert.strictEqual(backupFileService.hasBackupSync(resource), true); + + customWorkingCopy.setDirty(false); + customWorkingCopy.setDirty(true); + await backupFileService.joinBackupResource(); + assert.strictEqual(backupFileService.hasBackupSync(resource), true); + + customWorkingCopy.setDirty(false); + await backupFileService.joinDiscardBackup(); + assert.strictEqual(backupFileService.hasBackupSync(resource), false); + + // Cancellation + customWorkingCopy.setDirty(true); + await timeout(0); + customWorkingCopy.setDirty(false); + await backupFileService.joinDiscardBackup(); + assert.strictEqual(backupFileService.hasBackupSync(resource), false); + + customWorkingCopy.dispose(); + await cleanup(); + }); +}); diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts index efb89fafb..010507834 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts @@ -9,7 +9,7 @@ import * as os from 'os'; import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { hashPath } from 'vs/workbench/services/backup/electron-browser/backupFileService'; import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-sandbox/backupTracker'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; @@ -18,7 +18,7 @@ import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorInput, IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; @@ -28,7 +28,7 @@ import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/ele import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { toResource } from 'vs/base/test/common/utils'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IWorkingCopyBackup, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILogService } from 'vs/platform/log/common/log'; import { HotExitConfiguration } from 'vs/platform/files/common/files'; import { ShutdownReason, ILifecycleService, BeforeShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -38,24 +38,14 @@ import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestFilesConfigurationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { timeout } from 'vs/base/common/async'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; - -const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); -const backupHome = path.join(userdataDir, 'Backups'); -const workspacesJsonPath = path.join(backupHome, 'workspaces.json'); - -const workspaceResource = URI.file(platform.isWindows ? 'c:\\workspace' : '/workspace'); -const workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource)); +import { IProgressService } from 'vs/platform/progress/common/progress'; class TestBackupTracker extends NativeBackupTracker { @@ -70,14 +60,22 @@ class TestBackupTracker extends NativeBackupTracker { @INativeHostService nativeHostService: INativeHostService, @ILogService logService: ILogService, @IEditorService editorService: IEditorService, - @IEnvironmentService environmentService: IEnvironmentService + @IEnvironmentService environmentService: IEnvironmentService, + @IProgressService progressService: IProgressService ) { - super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, fileDialogService, dialogService, contextService, nativeHostService, logService, editorService, environmentService); + super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, fileDialogService, dialogService, contextService, nativeHostService, logService, editorService, environmentService, progressService); } protected getBackupScheduleDelay(): number { return 10; // Reduce timeout for tests } + + dispose() { + super.dispose(); + for (const [_, disposable] of this.pendingBackups) { + disposable.dispose(); + } + } } class BeforeShutdownEventImpl implements BeforeShutdownEvent { @@ -90,11 +88,22 @@ class BeforeShutdownEventImpl implements BeforeShutdownEvent { } } -suite('BackupTracker', () => { +flakySuite('BackupTracker (native)', function () { + let testDir: string; + let backupHome: string; + let workspaceBackupPath: string; + let accessor: TestServiceAccessor; let disposables: IDisposable[] = []; setup(async () => { + testDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); + backupHome = path.join(testDir, 'Backups'); + const workspacesJsonPath = path.join(backupHome, 'workspaces.json'); + + const workspaceResource = URI.file(platform.isWindows ? 'c:\\workspace' : '/workspace'); + workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource)); + const instantiationService = workbenchInstantiationService(); accessor = instantiationService.createInstance(TestServiceAccessor); @@ -107,8 +116,6 @@ suite('BackupTracker', () => { [new SyncDescriptor(FileEditorInput)] )); - // Delete any existing backups completely and then re-create it. - await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); await pfs.mkdirp(backupHome); await pfs.mkdirp(workspaceBackupPath); @@ -121,11 +128,11 @@ suite('BackupTracker', () => { (accessor.textFileService.files).dispose(); - return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); + return pfs.rimraf(testDir); }); - async function createTracker(autoSaveEnabled = false): Promise<[TestServiceAccessor, EditorPart, BackupTracker, IInstantiationService]> { - const backupFileService = new NodeTestBackupFileService(workspaceBackupPath); + async function createTracker(autoSaveEnabled = false): Promise<{ accessor: TestServiceAccessor, part: EditorPart, tracker: BackupTracker, instantiationService: IInstantiationService, cleanup: () => Promise }> { + const backupFileService = new NodeTestBackupFileService(testDir, workspaceBackupPath); const instantiationService = workbenchInstantiationService(); instantiationService.stub(IBackupFileService, backupFileService); @@ -155,50 +162,19 @@ suite('BackupTracker', () => { const tracker = instantiationService.createInstance(TestBackupTracker); - return [accessor, part, tracker, instantiationService]; + const cleanup = async () => { + // File changes could also schedule some backup operations so we need to wait for them before finishing the test + await accessor.backupFileService.waitForAllBackups(); + + part.dispose(); + tracker.dispose(); + }; + + return { accessor, part, tracker, instantiationService, cleanup }; } - async function untitledBackupTest(untitled: IUntitledTextResourceEditorInput = {}): Promise { - const [accessor, part, tracker] = await createTracker(); - - const untitledEditor = (await accessor.editorService.openEditor(untitled))?.input as UntitledTextEditorInput; - - const untitledModel = await untitledEditor.resolve(); - - if (!untitled?.contents) { - untitledModel.textEditorModel.setValue('Super Good'); - } - - await accessor.backupFileService.joinBackupResource(); - - assert.equal(accessor.backupFileService.hasBackupSync(untitledEditor.resource), true); - - untitledModel.dispose(); - - await accessor.backupFileService.joinDiscardBackup(); - - assert.equal(accessor.backupFileService.hasBackupSync(untitledEditor.resource), false); - - part.dispose(); - tracker.dispose(); - } - - test('Track backups (untitled)', function () { - this.timeout(20000); - - return untitledBackupTest(); - }); - - test('Track backups (untitled with initial contents)', function () { - this.timeout(20000); - - return untitledBackupTest({ contents: 'Foo Bar' }); - }); - test('Track backups (file)', async function () { - this.timeout(20000); - - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -208,69 +184,19 @@ suite('BackupTracker', () => { await accessor.backupFileService.joinBackupResource(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), true); + assert.strictEqual(accessor.backupFileService.hasBackupSync(resource), true); fileModel?.dispose(); await accessor.backupFileService.joinDiscardBackup(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), false); + assert.strictEqual(accessor.backupFileService.hasBackupSync(resource), false); - part.dispose(); - tracker.dispose(); - }); - - test('Track backups (custom)', async function () { - const [accessor, part, tracker] = await createTracker(); - - class TestBackupWorkingCopy extends TestWorkingCopy { - - backupDelay = 0; - - constructor(resource: URI) { - super(resource); - - accessor.workingCopyService.registerWorkingCopy(this); - } - - async backup(token: CancellationToken): Promise { - await timeout(this.backupDelay); - - return {}; - } - } - - const resource = toResource.call(this, '/path/custom.txt'); - const customWorkingCopy = new TestBackupWorkingCopy(resource); - - // Normal - customWorkingCopy.setDirty(true); - await accessor.backupFileService.joinBackupResource(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), true); - - customWorkingCopy.setDirty(false); - customWorkingCopy.setDirty(true); - await accessor.backupFileService.joinBackupResource(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), true); - - customWorkingCopy.setDirty(false); - await accessor.backupFileService.joinDiscardBackup(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), false); - - // Cancellation - customWorkingCopy.setDirty(true); - await timeout(0); - customWorkingCopy.setDirty(false); - await accessor.backupFileService.joinDiscardBackup(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), false); - - customWorkingCopy.dispose(); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - no veto if no dirty files', async function () { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -281,12 +207,11 @@ suite('BackupTracker', () => { const veto = await event.value; assert.ok(!veto); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - veto if user cancels (hot.exit: off)', async function () { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -298,7 +223,7 @@ suite('BackupTracker', () => { await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); @@ -306,12 +231,11 @@ suite('BackupTracker', () => { const veto = await event.value; assert.ok(veto); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - no veto if auto save is on', async function () { - const [accessor, part, tracker] = await createTracker(true /* auto save enabled */); + const { accessor, cleanup } = await createTracker(true /* auto save enabled */); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -320,7 +244,7 @@ suite('BackupTracker', () => { await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); @@ -328,14 +252,13 @@ suite('BackupTracker', () => { const veto = await event.value; assert.ok(!veto); - assert.equal(accessor.workingCopyService.dirtyCount, 0); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', async function () { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -347,7 +270,7 @@ suite('BackupTracker', () => { await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); @@ -355,12 +278,11 @@ suite('BackupTracker', () => { assert.ok(!veto); assert.ok(accessor.backupFileService.discardedBackups.length > 0); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - save (hot.exit: off)', async function () { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -372,7 +294,7 @@ suite('BackupTracker', () => { await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); @@ -380,8 +302,7 @@ suite('BackupTracker', () => { assert.ok(!veto); assert.ok(!model?.isDirty()); - part.dispose(); - tracker.dispose(); + await cleanup(); }); suite('Hot Exit', () => { @@ -488,7 +409,7 @@ suite('BackupTracker', () => { }); async function hotExitTest(this: any, setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: boolean, shouldVeto: boolean): Promise { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -513,18 +434,17 @@ suite('BackupTracker', () => { await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); event.reason = shutdownReason; accessor.lifecycleService.fireWillShutdown(event); const veto = await event.value; - assert.equal(accessor.backupFileService.discardedBackups.length, 0); // When hot exit is set, backups should never be cleaned since the confirm result is cancel - assert.equal(veto, shouldVeto); + assert.strictEqual(accessor.backupFileService.discardedBackups.length, 0); // When hot exit is set, backups should never be cleaned since the confirm result is cancel + assert.strictEqual(veto, shouldVeto); - part.dispose(); - tracker.dispose(); + await cleanup(); } }); }); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index ffc54667e..889c25a91 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -19,6 +19,8 @@ import { BulkCellEdits, ResourceNotebookCellEdit } from 'vs/workbench/contrib/bu import { UndoRedoGroup, UndoRedoSource } from 'vs/platform/undoRedo/common/undoRedo'; import { LinkedList } from 'vs/base/common/linkedList'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; class BulkEdit { @@ -30,6 +32,7 @@ class BulkEdit { private readonly _edits: ResourceEdit[], private readonly _undoRedoGroup: UndoRedoGroup, private readonly _undoRedoSource: UndoRedoSource | undefined, + private readonly _confirmBeforeUndo: boolean, @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private readonly _logService: ILogService, ) { @@ -76,7 +79,7 @@ class BulkEdit { } const group = this._edits.slice(index, index + range); if (group[0] instanceof ResourceFileEdit) { - await this._performFileEdits(group, this._undoRedoGroup, this._undoRedoSource, progress); + await this._performFileEdits(group, this._undoRedoGroup, this._undoRedoSource, this._confirmBeforeUndo, progress); } else if (group[0] instanceof ResourceTextEdit) { await this._performTextEdits(group, this._undoRedoGroup, this._undoRedoSource, progress); } else if (group[0] instanceof ResourceNotebookCellEdit) { @@ -88,9 +91,9 @@ class BulkEdit { } } - private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress) { + private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, confirmBeforeUndo: boolean, progress: IProgress) { this._logService.debug('_performFileEdits', JSON.stringify(edits)); - const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), undoRedoGroup, undoRedoSource, progress, this._token, edits); + const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), undoRedoGroup, undoRedoSource, confirmBeforeUndo, progress, this._token, edits); await model.apply(); } @@ -118,6 +121,8 @@ export class BulkEditService implements IBulkEditService { @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private readonly _logService: ILogService, @IEditorService private readonly _editorService: IEditorService, + @ILifecycleService private readonly _lifecycleService: ILifecycleService, + @IDialogService private readonly _dialogService: IDialogService ) { } setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable { @@ -139,7 +144,7 @@ export class BulkEditService implements IBulkEditService { return { ariaSummary: localize('nothing', "Made no edits") }; } - if (this._previewHandler && !options?.suppressPreview && (options?.showPreview || edits.some(value => value.metadata?.needsConfirmation))) { + if (this._previewHandler && (options?.showPreview || edits.some(value => value.metadata?.needsConfirmation))) { edits = await this._previewHandler(edits, options); } @@ -175,18 +180,22 @@ export class BulkEditService implements IBulkEditService { undoRedoGroupRemove = this._activeUndoRedoGroups.push(undoRedoGroup); } + const label = options?.quotableLabel || options?.label; const bulkEdit = this._instaService.createInstance( BulkEdit, - options?.quotableLabel || options?.label, + label, codeEditor, options?.progress ?? Progress.None, options?.token ?? CancellationToken.None, edits, undoRedoGroup, - options?.undoRedoSource + options?.undoRedoSource, + !!options?.confirmBeforeUndo ); + let listener: IDisposable | undefined; try { + listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this.shouldVeto(label), 'veto.blukEditService')); await bulkEdit.perform(); return { ariaSummary: bulkEdit.ariaMessage() }; } catch (err) { @@ -195,9 +204,20 @@ export class BulkEditService implements IBulkEditService { this._logService.error(err); throw err; } finally { + listener?.dispose(); undoRedoGroupRemove(); } } + + private async shouldVeto(label: string | undefined): Promise { + label = label || localize('fileOperation', "File operation"); + const result = await this._dialogService.confirm({ + message: localize('areYouSureQuiteBulkEdit', "Are you sure you want to quit? '{0}' is in progress.", label), + primaryButton: localize('quit', "Quit") + }); + + return !result.confirmed; + } } registerSingleton(IBulkEditService, BulkEditService, true); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index f4ac30ca0..e15f322a5 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -8,20 +8,16 @@ import { WorkspaceFileEditOptions } from 'vs/editor/common/modes'; import { IFileService, FileSystemProviderCapabilities, IFileContent } from 'vs/platform/files/common/files'; import { IProgress } from 'vs/platform/progress/common/progress'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IWorkingCopyFileService, IFileOperationUndoRedoInfo, IMoveOperation, ICopyOperation, IDeleteOperation, ICreateOperation, ICreateFileOperation } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoService, UndoRedoGroup, UndoRedoSource } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { VSBuffer } from 'vs/base/common/buffer'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; -import * as resources from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; - -interface IFileOperationUndoRedoInfo { - undoRedoGroupId?: number; - isUndoing?: boolean; -} +import { flatten, tail } from 'vs/base/common/arrays'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; interface IFileOperation { uris: URI[]; @@ -36,115 +32,189 @@ class Noop implements IFileOperation { } } -class RenameOperation implements IFileOperation { - +class RenameEdit { + readonly type = 'rename'; constructor( readonly newUri: URI, readonly oldUri: URI, - readonly options: WorkspaceFileEditOptions, - readonly undoRedoInfo: IFileOperationUndoRedoInfo, + readonly options: WorkspaceFileEditOptions + ) { } +} + +class RenameOperation implements IFileOperation { + + constructor( + private readonly _edits: RenameEdit[], + private readonly _undoRedoInfo: IFileOperationUndoRedoInfo, @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IFileService private readonly _fileService: IFileService, ) { } get uris() { - return [this.newUri, this.oldUri]; + return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri])); } async perform(token: CancellationToken): Promise { - // rename - if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) { - return new Noop(); // not overwriting, but ignoring, and the target file exists + + const moves: IMoveOperation[] = []; + const undoes: RenameEdit[] = []; + for (const edit of this._edits) { + // check: not overwriting, but ignoring, and the target file exists + const skip = edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri); + if (!skip) { + moves.push({ + file: { source: edit.oldUri, target: edit.newUri }, + overwrite: edit.options.overwrite + }); + + // reverse edit + undoes.push(new RenameEdit(edit.oldUri, edit.newUri, edit.options)); + } } - await this._workingCopyFileService.move([{ source: this.oldUri, target: this.newUri }], { overwrite: this.options.overwrite, ...this.undoRedoInfo }, token); - return new RenameOperation(this.oldUri, this.newUri, this.options, { isUndoing: true }, this._workingCopyFileService, this._fileService); + if (moves.length === 0) { + return new Noop(); + } + + await this._workingCopyFileService.move(moves, this._undoRedoInfo, token); + return new RenameOperation(undoes, { isUndoing: true }, this._workingCopyFileService, this._fileService); } toString(): string { - const oldBasename = resources.basename(this.oldUri); - const newBasename = resources.basename(this.newUri); - if (oldBasename !== newBasename) { - return `(rename ${oldBasename} to ${newBasename})`; - } - return `(rename ${this.oldUri} to ${this.newUri})`; + return `(rename ${this._edits.map(edit => `${edit.oldUri} to ${edit.newUri}`).join(', ')})`; } } +class CopyEdit { + readonly type = 'copy'; + constructor( + readonly newUri: URI, + readonly oldUri: URI, + readonly options: WorkspaceFileEditOptions + ) { } +} + class CopyOperation implements IFileOperation { constructor( - readonly newUri: URI, - readonly oldUri: URI, - readonly options: WorkspaceFileEditOptions, - readonly undoRedoInfo: IFileOperationUndoRedoInfo, + private readonly _edits: CopyEdit[], + private readonly _undoRedoInfo: IFileOperationUndoRedoInfo, @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IFileService private readonly _fileService: IFileService, @IInstantiationService private readonly _instaService: IInstantiationService ) { } get uris() { - return [this.newUri, this.oldUri]; + return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri])); } async perform(token: CancellationToken): Promise { - // copy - if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) { - return new Noop(); // not overwriting, but ignoring, and the target file exists + + // (1) create copy operations, remove noops + const copies: ICopyOperation[] = []; + for (const edit of this._edits) { + //check: not overwriting, but ignoring, and the target file exists + const skip = edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri); + if (!skip) { + copies.push({ file: { source: edit.oldUri, target: edit.newUri }, overwrite: edit.options.overwrite }); + } } - const stat = await this._workingCopyFileService.copy([{ source: this.oldUri, target: this.newUri }], { overwrite: this.options.overwrite, ...this.undoRedoInfo }, token); - const folder = this.options.folder || (stat.length === 1 && stat[0].isDirectory); - return this._instaService.createInstance(DeleteOperation, this.newUri, { recursive: true, folder, ...this.options }, { isUndoing: true }, false); + if (copies.length === 0) { + return new Noop(); + } + + // (2) perform the actual copy and use the return stats to build undo edits + const stats = await this._workingCopyFileService.copy(copies, this._undoRedoInfo, token); + const undoes: DeleteEdit[] = []; + + for (let i = 0; i < stats.length; i++) { + const stat = stats[i]; + const edit = this._edits[i]; + undoes.push(new DeleteEdit(stat.resource, { recursive: true, folder: this._edits[i].options.folder || stat.isDirectory, ...edit.options }, false)); + } + + return this._instaService.createInstance(DeleteOperation, undoes, { isUndoing: true }); } toString(): string { - return `(copy ${this.oldUri} to ${this.newUri})`; + return `(copy ${this._edits.map(edit => `${edit.oldUri} to ${edit.newUri}`).join(', ')})`; } } +class CreateEdit { + readonly type = 'create'; + constructor( + readonly newUri: URI, + readonly options: WorkspaceFileEditOptions, + readonly contents: VSBuffer | undefined, + ) { } +} + class CreateOperation implements IFileOperation { constructor( - readonly newUri: URI, - readonly options: WorkspaceFileEditOptions, - readonly undoRedoInfo: IFileOperationUndoRedoInfo, - readonly contents: VSBuffer | undefined, + private readonly _edits: CreateEdit[], + private readonly _undoRedoInfo: IFileOperationUndoRedoInfo, @IFileService private readonly _fileService: IFileService, @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IInstantiationService private readonly _instaService: IInstantiationService, + @ITextFileService private readonly _textFileService: ITextFileService ) { } get uris() { - return [this.newUri]; + return this._edits.map(edit => edit.newUri); } async perform(token: CancellationToken): Promise { - // create file - if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) { - return new Noop(); // not overwriting, but ignoring, and the target file exists + + const folderCreates: ICreateOperation[] = []; + const fileCreates: ICreateFileOperation[] = []; + const undoes: DeleteEdit[] = []; + + for (const edit of this._edits) { + if (edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri)) { + continue; // not overwriting, but ignoring, and the target file exists + } + if (edit.options.folder) { + folderCreates.push({ resource: edit.newUri }); + } else { + // If the contents are part of the edit they include the encoding, thus use them. Otherwise get the encoding for a new empty file. + const encodedReadable = typeof edit.contents !== 'undefined' ? edit.contents : await this._textFileService.getEncodedReadable(edit.newUri); + fileCreates.push({ resource: edit.newUri, contents: encodedReadable, overwrite: edit.options.overwrite }); + } + undoes.push(new DeleteEdit(edit.newUri, edit.options, !edit.options.folder && !edit.contents)); } - if (this.options.folder) { - await this._workingCopyFileService.createFolder(this.newUri, { ...this.undoRedoInfo }, token); - } else { - await this._workingCopyFileService.create(this.newUri, this.contents, { overwrite: this.options.overwrite, ...this.undoRedoInfo }, token); + + if (folderCreates.length === 0 && fileCreates.length === 0) { + return new Noop(); } - return this._instaService.createInstance(DeleteOperation, this.newUri, this.options, { isUndoing: true }, !this.options.folder && !this.contents); + + await this._workingCopyFileService.createFolder(folderCreates, this._undoRedoInfo, token); + await this._workingCopyFileService.create(fileCreates, this._undoRedoInfo, token); + + return this._instaService.createInstance(DeleteOperation, undoes, { isUndoing: true }); } toString(): string { - return this.options.folder ? `create ${resources.basename(this.newUri)} folder` - : `(create ${resources.basename(this.newUri)} with ${this.contents?.byteLength || 0} bytes)`; + return `(create ${this._edits.map(edit => edit.options.folder ? `folder ${edit.newUri}` : `file ${edit.newUri} with ${edit.contents?.byteLength || 0} bytes`).join(', ')})`; } } +class DeleteEdit { + readonly type = 'delete'; + constructor( + readonly oldUri: URI, + readonly options: WorkspaceFileEditOptions, + readonly undoesCreate: boolean, + ) { } +} + class DeleteOperation implements IFileOperation { constructor( - readonly oldUri: URI, - readonly options: WorkspaceFileEditOptions, - readonly undoRedoInfo: IFileOperationUndoRedoInfo, - private readonly _undoesCreateOperation: boolean, + private _edits: DeleteEdit[], + private readonly _undoRedoInfo: IFileOperationUndoRedoInfo, @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IFileService private readonly _fileService: IFileService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -153,38 +223,58 @@ class DeleteOperation implements IFileOperation { ) { } get uris() { - return [this.oldUri]; + return this._edits.map(edit => edit.oldUri); } async perform(token: CancellationToken): Promise { // delete file - if (!await this._fileService.exists(this.oldUri)) { - if (!this.options.ignoreIfNotExists) { - throw new Error(`${this.oldUri} does not exist and can not be deleted`); - } - return new Noop(); - } - let fileContent: IFileContent | undefined; - if (!this._undoesCreateOperation && !this.options.folder) { - try { - fileContent = await this._fileService.readFile(this.oldUri); - } catch (err) { - this._logService.critical(err); + const deletes: IDeleteOperation[] = []; + const undoes: CreateEdit[] = []; + + for (const edit of this._edits) { + if (!await this._fileService.exists(edit.oldUri)) { + if (!edit.options.ignoreIfNotExists) { + throw new Error(`${edit.oldUri} does not exist and can not be deleted`); + } + continue; + } + + deletes.push({ + resource: edit.oldUri, + recursive: edit.options.recursive, + useTrash: !edit.options.skipTrashBin && this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue('files.enableTrash') + }); + + + // read file contents for undo operation. when a file is too large it won't be restored + let fileContent: IFileContent | undefined; + if (!edit.undoesCreate && !edit.options.folder) { + try { + fileContent = await this._fileService.readFile(edit.oldUri); + } catch (err) { + this._logService.critical(err); + } + } + if (!(typeof edit.options.maxSize === 'number' && fileContent && (fileContent?.size > edit.options.maxSize))) { + undoes.push(new CreateEdit(edit.oldUri, edit.options, fileContent?.value)); } } - const useTrash = !this.options.skipTrashBin && this._fileService.hasCapability(this.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue('files.enableTrash'); - await this._workingCopyFileService.delete([this.oldUri], { useTrash, recursive: this.options.recursive, ...this.undoRedoInfo }, token); - - if (typeof this.options.maxSize === 'number' && fileContent && (fileContent?.size > this.options.maxSize)) { + if (deletes.length === 0) { return new Noop(); } - return this._instaService.createInstance(CreateOperation, this.oldUri, this.options, { isUndoing: true }, fileContent?.value); + + await this._workingCopyFileService.delete(deletes, this._undoRedoInfo, token); + + if (undoes.length === 0) { + return new Noop(); + } + return this._instaService.createInstance(CreateOperation, undoes, { isUndoing: true }); } toString(): string { - return `(delete ${resources.basename(this.oldUri)})`; + return `(delete ${this._edits.map(edit => edit.oldUri).join(', ')})`; } } @@ -196,7 +286,8 @@ class FileUndoRedoElement implements IWorkspaceUndoRedoElement { constructor( readonly label: string, - readonly operations: IFileOperation[] + readonly operations: IFileOperation[], + readonly confirmBeforeUndo: boolean ) { this.resources = ([]).concat(...operations.map(op => op.uris)); } @@ -217,7 +308,7 @@ class FileUndoRedoElement implements IWorkspaceUndoRedoElement { } } - public toString(): string { + toString(): string { return this.operations.map(op => String(op)).join(', '); } } @@ -228,6 +319,7 @@ export class BulkFileEdits { private readonly _label: string, private readonly _undoRedoGroup: UndoRedoGroup, private readonly _undoRedoSource: UndoRedoSource | undefined, + private readonly _confirmBeforeUndo: boolean, private readonly _progress: IProgress, private readonly _token: CancellationToken, private readonly _edits: ResourceFileEdit[], @@ -238,34 +330,66 @@ export class BulkFileEdits { async apply(): Promise { const undoOperations: IFileOperation[] = []; const undoRedoInfo = { undoRedoGroupId: this._undoRedoGroup.id }; + + const edits: Array = []; for (const edit of this._edits) { + if (edit.newResource && edit.oldResource && !edit.options?.copy) { + edits.push(new RenameEdit(edit.newResource, edit.oldResource, edit.options ?? {})); + } else if (edit.newResource && edit.oldResource && edit.options?.copy) { + edits.push(new CopyEdit(edit.newResource, edit.oldResource, edit.options ?? {})); + } else if (!edit.newResource && edit.oldResource) { + edits.push(new DeleteEdit(edit.oldResource, edit.options ?? {}, false)); + } else if (edit.newResource && !edit.oldResource) { + edits.push(new CreateEdit(edit.newResource, edit.options ?? {}, undefined)); + } + } + + if (edits.length === 0) { + return; + } + + const groups: Array[] = []; + groups[0] = [edits[0]]; + + for (let i = 1; i < edits.length; i++) { + const edit = edits[i]; + const lastGroup = tail(groups); + if (lastGroup[0].type === edit.type) { + lastGroup.push(edit); + } else { + groups.push([edit]); + } + } + + for (let group of groups) { if (this._token.isCancellationRequested) { break; } - const options = edit.options || {}; let op: IFileOperation | undefined; - if (edit.newResource && edit.oldResource && !options.copy) { - // rename - op = this._instaService.createInstance(RenameOperation, edit.newResource, edit.oldResource, options, undoRedoInfo); - } else if (edit.newResource && edit.oldResource && options.copy) { - op = this._instaService.createInstance(CopyOperation, edit.newResource, edit.oldResource, options, undoRedoInfo); - } else if (!edit.newResource && edit.oldResource) { - // delete file - op = this._instaService.createInstance(DeleteOperation, edit.oldResource, options, undoRedoInfo, false); - } else if (edit.newResource && !edit.oldResource) { - // create file - op = this._instaService.createInstance(CreateOperation, edit.newResource, options, undoRedoInfo, undefined); + switch (group[0].type) { + case 'rename': + op = this._instaService.createInstance(RenameOperation, group, undoRedoInfo); + break; + case 'copy': + op = this._instaService.createInstance(CopyOperation, group, undoRedoInfo); + break; + case 'delete': + op = this._instaService.createInstance(DeleteOperation, group, undoRedoInfo); + break; + case 'create': + op = this._instaService.createInstance(CreateOperation, group, undoRedoInfo); + break; } + if (op) { const undoOp = await op.perform(this._token); undoOperations.push(undoOp); } - this._progress.report(undefined); } - this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, undoOperations), this._undoRedoGroup, this._undoRedoSource); + this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, undoOperations, this._confirmBeforeUndo), this._undoRedoGroup, this._undoRedoSource); } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index ab0172eec..3b35a67b7 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -17,7 +17,7 @@ import { BulkEditPreviewProvider, BulkFileOperations, BulkFileOperationType } fr import { ILabelService } from 'vs/platform/label/common/label'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { URI } from 'vs/base/common/uri'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index e9dddb2a2..7d1348b25 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -409,14 +409,14 @@ export class CategoryElementRenderer implements ITreeRenderer('accessibilityHelpWidgetVisible', false); -class AccessibilityHelpController extends Disposable implements IEditorContribution { +export class AccessibilityHelpController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.accessibilityHelpController'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index c3ede411a..0f7a8480b 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -8,10 +8,10 @@ import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { registerDiffEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IDiffEditorContribution } from 'vs/editor/common/editorCommon'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; +import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { IDiffComputationResult } from 'vs/editor/common/services/editorWorkerService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; const enum WidgetState { @@ -48,7 +48,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont Severity.Warning, nls.localize('hintTimeout', "The diff algorithm was stopped early (after {0} ms.)", this._diffEditor.maxComputationTime), [{ - label: nls.localize('removeTimeout', "Remove limit"), + label: nls.localize('removeTimeout', "Remove Limit"), run: () => { this._configurationService.updateValue('diffEditor.maxComputationTime', 0); } @@ -94,7 +94,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont private _onDidClickHelperWidget(): void { if (this._state === WidgetState.HintWhitespace) { - this._configurationService.updateValue('diffEditor.ignoreTrimWhitespace', false, ConfigurationTarget.USER); + this._configurationService.updateValue('diffEditor.ignoreTrimWhitespace', false); } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts index 69ee09504..657af6f1b 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts @@ -9,7 +9,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { LanguageIdentifier } from 'vs/editor/common/modes'; -import { CharacterPair, CommentRule, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentationRule, LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration'; +import { CharacterPair, CommentRule, EnterAction, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentAction, IndentationRule, LanguageConfiguration, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { IModeService } from 'vs/editor/common/services/modeService'; import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; @@ -31,6 +31,19 @@ interface IIndentationRules { unIndentedLinePattern?: string | IRegExp; } +interface IEnterAction { + indent: 'none' | 'indent' | 'indentOutdent' | 'outdent'; + appendText?: string; + removeText?: number; +} + +interface IOnEnterRule { + beforeText: string | IRegExp; + afterText?: string | IRegExp; + previousLineText?: string | IRegExp; + action: IEnterAction; +} + interface ILanguageConfiguration { comments?: CommentRule; brackets?: CharacterPair[]; @@ -40,6 +53,7 @@ interface ILanguageConfiguration { indentationRules?: IIndentationRules; folding?: FoldingRules; autoCloseBefore?: string; + onEnterRules?: IOnEnterRule[]; } function isStringArr(something: string[] | null): something is string[] { @@ -93,7 +107,7 @@ export class LanguageConfigurationFileHandler { } this._done[languageIdentifier.id] = true; - let configurationFiles = this._modeService.getConfigurationFiles(languageIdentifier.language); + const configurationFiles = this._modeService.getConfigurationFiles(languageIdentifier.language); configurationFiles.forEach((configFileLocation) => this._handleConfigFile(languageIdentifier, configFileLocation)); } @@ -254,18 +268,82 @@ export class LanguageConfigurationFileHandler { return result; } - // private _mapCharacterPairs(pairs: Array): IAutoClosingPairConditional[] { - // return pairs.map(pair => { - // if (Array.isArray(pair)) { - // return { open: pair[0], close: pair[1] }; - // } - // return pair; - // }); - // } + private _extractValidOnEnterRules(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): OnEnterRule[] | null { + const source = configuration.onEnterRules; + if (typeof source === 'undefined') { + return null; + } + if (!Array.isArray(source)) { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules\` to be an array.`); + return null; + } + + let result: OnEnterRule[] | null = null; + for (let i = 0, len = source.length; i < len; i++) { + const onEnterRule = source[i]; + if (!types.isObject(onEnterRule)) { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}]\` to be an object.`); + continue; + } + if (!types.isObject(onEnterRule.action)) { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action\` to be an object.`); + continue; + } + let indentAction: IndentAction; + if (onEnterRule.action.indent === 'none') { + indentAction = IndentAction.None; + } else if (onEnterRule.action.indent === 'indent') { + indentAction = IndentAction.Indent; + } else if (onEnterRule.action.indent === 'indentOutdent') { + indentAction = IndentAction.IndentOutdent; + } else if (onEnterRule.action.indent === 'outdent') { + indentAction = IndentAction.Outdent; + } else { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.indent\` to be 'none', 'indent', 'indentOutdent' or 'outdent'.`); + continue; + } + const action: EnterAction = { indentAction }; + if (onEnterRule.action.appendText) { + if (typeof onEnterRule.action.appendText === 'string') { + action.appendText = onEnterRule.action.appendText; + } else { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.appendText\` to be undefined or a string.`); + } + } + if (onEnterRule.action.removeText) { + if (typeof onEnterRule.action.removeText === 'number') { + action.removeText = onEnterRule.action.removeText; + } else { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.removeText\` to be undefined or a number.`); + } + } + const beforeText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].beforeText`, onEnterRule.beforeText); + if (!beforeText) { + continue; + } + const resultingOnEnterRule: OnEnterRule = { beforeText, action }; + if (onEnterRule.afterText) { + const afterText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].afterText`, onEnterRule.afterText); + if (afterText) { + resultingOnEnterRule.afterText = afterText; + } + } + if (onEnterRule.previousLineText) { + const previousLineText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].previousLineText`, onEnterRule.previousLineText); + if (previousLineText) { + resultingOnEnterRule.previousLineText = previousLineText; + } + } + result = result || []; + result.push(resultingOnEnterRule); + } + + return result; + } private _handleConfig(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): void { - let richEditConfig: LanguageConfiguration = {}; + const richEditConfig: LanguageConfiguration = {}; const comments = this._extractValidCommentRule(languageIdentifier, configuration); if (comments) { @@ -293,25 +371,21 @@ export class LanguageConfigurationFileHandler { } if (configuration.wordPattern) { - try { - let wordPattern = this._parseRegex(configuration.wordPattern); - if (wordPattern) { - richEditConfig.wordPattern = wordPattern; - } - } catch (error) { - // Malformed regexes are ignored + const wordPattern = this._parseRegex(languageIdentifier, `wordPattern`, configuration.wordPattern); + if (wordPattern) { + richEditConfig.wordPattern = wordPattern; } } if (configuration.indentationRules) { - let indentationRules = this._mapIndentationRules(configuration.indentationRules); + const indentationRules = this._mapIndentationRules(languageIdentifier, configuration.indentationRules); if (indentationRules) { richEditConfig.indentationRules = indentationRules; } } if (configuration.folding) { - let markers = configuration.folding.markers; + const markers = configuration.folding.markers; richEditConfig.folding = { offSide: configuration.folding.offSide, @@ -319,44 +393,66 @@ export class LanguageConfigurationFileHandler { }; } + const onEnterRules = this._extractValidOnEnterRules(languageIdentifier, configuration); + if (onEnterRules) { + richEditConfig.onEnterRules = onEnterRules; + } + LanguageConfigurationRegistry.register(languageIdentifier, richEditConfig); } - private _parseRegex(value: string | IRegExp) { + private _parseRegex(languageIdentifier: LanguageIdentifier, confPath: string, value: string | IRegExp) { if (typeof value === 'string') { - return new RegExp(value, ''); - } else if (typeof value === 'object') { - return new RegExp(value.pattern, value.flags); + try { + return new RegExp(value, ''); + } catch (err) { + console.warn(`[${languageIdentifier.language}]: Invalid regular expression in \`${confPath}\`: `, err); + return null; + } } - + if (types.isObject(value)) { + if (typeof value.pattern !== 'string') { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}.pattern\` to be a string.`); + return null; + } + if (typeof value.flags !== 'undefined' && typeof value.flags !== 'string') { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}.flags\` to be a string.`); + return null; + } + try { + return new RegExp(value.pattern, value.flags); + } catch (err) { + console.warn(`[${languageIdentifier.language}]: Invalid regular expression in \`${confPath}\`: `, err); + return null; + } + } + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}\` to be a string or an object.`); return null; } - private _mapIndentationRules(indentationRules: IIndentationRules): IndentationRule | null { - try { - let increaseIndentPattern = this._parseRegex(indentationRules.increaseIndentPattern); - let decreaseIndentPattern = this._parseRegex(indentationRules.decreaseIndentPattern); - - if (increaseIndentPattern && decreaseIndentPattern) { - let result: IndentationRule = { - increaseIndentPattern: increaseIndentPattern, - decreaseIndentPattern: decreaseIndentPattern - }; - - if (indentationRules.indentNextLinePattern) { - result.indentNextLinePattern = this._parseRegex(indentationRules.indentNextLinePattern); - } - if (indentationRules.unIndentedLinePattern) { - result.unIndentedLinePattern = this._parseRegex(indentationRules.unIndentedLinePattern); - } - - return result; - } - } catch (error) { - // Malformed regexes are ignored + private _mapIndentationRules(languageIdentifier: LanguageIdentifier, indentationRules: IIndentationRules): IndentationRule | null { + const increaseIndentPattern = this._parseRegex(languageIdentifier, `indentationRules.increaseIndentPattern`, indentationRules.increaseIndentPattern); + if (!increaseIndentPattern) { + return null; + } + const decreaseIndentPattern = this._parseRegex(languageIdentifier, `indentationRules.decreaseIndentPattern`, indentationRules.decreaseIndentPattern); + if (!decreaseIndentPattern) { + return null; } - return null; + const result: IndentationRule = { + increaseIndentPattern: increaseIndentPattern, + decreaseIndentPattern: decreaseIndentPattern + }; + + if (indentationRules.indentNextLinePattern) { + result.indentNextLinePattern = this._parseRegex(languageIdentifier, `indentationRules.indentNextLinePattern`, indentationRules.indentNextLinePattern); + } + if (indentationRules.unIndentedLinePattern) { + result.unIndentedLinePattern = this._parseRegex(languageIdentifier, `indentationRules.unIndentedLinePattern`, indentationRules.unIndentedLinePattern); + } + + return result; } } @@ -601,6 +697,101 @@ const schema: IJSONSchema = { } } } + }, + onEnterRules: { + type: 'array', + description: nls.localize('schema.onEnterRules', 'The language\'s rules to be evaluated when pressing Enter.'), + items: { + type: 'object', + description: nls.localize('schema.onEnterRules', 'The language\'s rules to be evaluated when pressing Enter.'), + required: ['beforeText', 'action'], + properties: { + beforeText: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.beforeText', 'This rule will only execute if the text before the cursor matches this regular expression.'), + properties: { + pattern: { + type: 'string', + description: nls.localize('schema.onEnterRules.beforeText.pattern', 'The RegExp pattern for beforeText.'), + default: '', + }, + flags: { + type: 'string', + description: nls.localize('schema.onEnterRules.beforeText.flags', 'The RegExp flags for beforeText.'), + default: '', + pattern: '^([gimuy]+)$', + patternErrorMessage: nls.localize('schema.onEnterRules.beforeText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.') + } + } + }, + afterText: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.afterText', 'This rule will only execute if the text after the cursor matches this regular expression.'), + properties: { + pattern: { + type: 'string', + description: nls.localize('schema.onEnterRules.afterText.pattern', 'The RegExp pattern for afterText.'), + default: '', + }, + flags: { + type: 'string', + description: nls.localize('schema.onEnterRules.afterText.flags', 'The RegExp flags for afterText.'), + default: '', + pattern: '^([gimuy]+)$', + patternErrorMessage: nls.localize('schema.onEnterRules.afterText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.') + } + } + }, + previousLineText: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.previousLineText', 'This rule will only execute if the text above the line matches this regular expression.'), + properties: { + pattern: { + type: 'string', + description: nls.localize('schema.onEnterRules.previousLineText.pattern', 'The RegExp pattern for previousLineText.'), + default: '', + }, + flags: { + type: 'string', + description: nls.localize('schema.onEnterRules.previousLineText.flags', 'The RegExp flags for previousLineText.'), + default: '', + pattern: '^([gimuy]+)$', + patternErrorMessage: nls.localize('schema.onEnterRules.previousLineText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.') + } + } + }, + action: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.action', 'The action to execute.'), + required: ['indent'], + default: { 'indent': 'indent' }, + properties: { + indent: { + type: 'string', + description: nls.localize('schema.onEnterRules.action.indent', "Describe what to do with the indentation"), + default: 'indent', + enum: ['none', 'indent', 'indentOutdent', 'outdent'], + markdownEnumDescriptions: [ + nls.localize('schema.onEnterRules.action.indent.none', "Insert new line and copy the previous line's indentation."), + nls.localize('schema.onEnterRules.action.indent.indent', "Insert new line and indent once (relative to the previous line's indentation)."), + nls.localize('schema.onEnterRules.action.indent.indentOutdent', "Insert two new lines:\n - the first one indented which will hold the cursor\n - the second one at the same indentation level"), + nls.localize('schema.onEnterRules.action.indent.outdent', "Insert new line and outdent once (relative to the previous line's indentation).") + ] + }, + appendText: { + type: 'string', + description: nls.localize('schema.onEnterRules.action.appendText', 'Describes text to be appended after the new line and after the indentation.'), + default: '', + }, + removeText: { + type: 'number', + description: nls.localize('schema.onEnterRules.action.removeText', 'Describes the number of characters to remove from the new line\'s indentation.'), + default: 0, + } + } + } + } + } } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts b/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts index f7a7ea98d..0d8174166 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts @@ -46,7 +46,7 @@ export class LargeFileOptimizationsWarner extends Disposable implements IEditorC this._notificationService.prompt(Severity.Info, message, [ { - label: nls.localize('removeOptimizations', "Forcefully enable features"), + label: nls.localize('removeOptimizations', "Forcefully Enable Features"), run: () => { this._configurationService.updateValue(`editor.largeFileOptimizations`, false).then(() => { this._notificationService.info(nls.localize('reopenFilePrompt', "Please reopen file in order for this setting to take effect.")); diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts new file mode 100644 index 000000000..bdb42efac --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts @@ -0,0 +1,429 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IBreadcrumbsDataSource, IOutline, IOutlineCreator, IOutlineListConfig, IOutlineService, OutlineChangeEvent, OutlineConfigKeys, OutlineTarget, } from 'vs/workbench/services/outline/browser/outline'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IEditorPane } from 'vs/workbench/common/editor'; +import { DocumentSymbolComparator, DocumentSymbolAccessibilityProvider, DocumentSymbolRenderer, DocumentSymbolFilter, DocumentSymbolGroupRenderer, DocumentSymbolIdentityProvider, DocumentSymbolNavigationLabelProvider, DocumentSymbolVirtualDelegate } from 'vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { OutlineGroup, OutlineElement, OutlineModel, TreeElement, IOutlineMarker } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { raceCancellation, TimeoutTimer, timeout, Barrier } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; +import { ITextModel } from 'vs/editor/common/model'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IPosition } from 'vs/editor/common/core/position'; +import { ScrollType } from 'vs/editor/common/editorCommon'; +import { Range } from 'vs/editor/common/core/range'; +import { IEditorOptions, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; +import { IDataSource } from 'vs/base/browser/ui/tree/tree'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { localize } from 'vs/nls'; +import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; +import { MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { isEqual } from 'vs/base/common/resources'; + +type DocumentSymbolItem = OutlineGroup | OutlineElement; + +class DocumentSymbolBreadcrumbsSource implements IBreadcrumbsDataSource{ + + private _breadcrumbs: (OutlineGroup | OutlineElement)[] = []; + + constructor( + private readonly _editor: ICodeEditor, + @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, + ) { } + + getBreadcrumbElements(): readonly DocumentSymbolItem[] { + return this._breadcrumbs; + } + + clear(): void { + this._breadcrumbs = []; + } + + update(model: OutlineModel, position: IPosition): void { + const newElements = this._computeBreadcrumbs(model, position); + this._breadcrumbs = newElements; + } + + private _computeBreadcrumbs(model: OutlineModel, position: IPosition): Array { + let item: OutlineGroup | OutlineElement | undefined = model.getItemEnclosingPosition(position); + if (!item) { + return []; + } + let chain: Array = []; + while (item) { + chain.push(item); + let parent: any = item.parent; + if (parent instanceof OutlineModel) { + break; + } + if (parent instanceof OutlineGroup && parent.parent && parent.parent.children.size === 1) { + break; + } + item = parent; + } + let result: Array = []; + for (let i = chain.length - 1; i >= 0; i--) { + let element = chain[i]; + if (this._isFiltered(element)) { + break; + } + result.push(element); + } + if (result.length === 0) { + return []; + } + return result; + } + + private _isFiltered(element: TreeElement): boolean { + if (!(element instanceof OutlineElement)) { + return false; + } + const key = `breadcrumbs.${DocumentSymbolFilter.kindToConfigName[element.symbol.kind]}`; + let uri: URI | undefined; + if (this._editor && this._editor.getModel()) { + const model = this._editor.getModel() as ITextModel; + uri = model.uri; + } + return !this._textResourceConfigurationService.getValue(uri, key); + } +} + +class DocumentSymbolsOutline implements IOutline { + + private readonly _disposables = new DisposableStore(); + private readonly _onDidChange = new Emitter(); + + readonly onDidChange: Event = this._onDidChange.event; + + private _outlineModel?: OutlineModel; + private _outlineDisposables = new DisposableStore(); + + private readonly _breadcrumbsDataSource: DocumentSymbolBreadcrumbsSource; + + readonly config: IOutlineListConfig; + + readonly outlineKind = 'documentSymbols'; + + get activeElement(): DocumentSymbolItem | undefined { + const posistion = this._editor.getPosition(); + if (!posistion || !this._outlineModel) { + return undefined; + } else { + return this._outlineModel.getItemEnclosingPosition(posistion); + } + } + + constructor( + private readonly _editor: ICodeEditor, + target: OutlineTarget, + firstLoadBarrier: Barrier, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + + this._breadcrumbsDataSource = new DocumentSymbolBreadcrumbsSource(_editor, textResourceConfigurationService); + const delegate = new DocumentSymbolVirtualDelegate(); + const renderers = [new DocumentSymbolGroupRenderer(), instantiationService.createInstance(DocumentSymbolRenderer, true)]; + const treeDataSource: IDataSource = { + getChildren: (parent) => { + if (parent instanceof OutlineElement || parent instanceof OutlineGroup) { + return parent.children.values(); + } + if (parent === this && this._outlineModel) { + return this._outlineModel.children.values(); + } + return []; + } + }; + const comparator = new DocumentSymbolComparator(); + const options = { + collapseByDefault: target === OutlineTarget.Breadcrumbs, + expandOnlyOnTwistieClick: true, + multipleSelectionSupport: false, + identityProvider: new DocumentSymbolIdentityProvider(), + keyboardNavigationLabelProvider: new DocumentSymbolNavigationLabelProvider(), + accessibilityProvider: new DocumentSymbolAccessibilityProvider(localize('document', "Document Symbols")), + filter: target === OutlineTarget.OutlinePane + ? instantiationService.createInstance(DocumentSymbolFilter, 'outline') + : target === OutlineTarget.Breadcrumbs + ? instantiationService.createInstance(DocumentSymbolFilter, 'breadcrumbs') + : undefined + }; + + this.config = { + breadcrumbsDataSource: this._breadcrumbsDataSource, + delegate, + renderers, + treeDataSource, + comparator, + options, + quickPickDataSource: { getQuickPickElements: () => { throw new Error('not implemented'); } } + }; + + + // update as language, model, providers changes + this._disposables.add(DocumentSymbolProviderRegistry.onDidChange(_ => this._createOutline())); + this._disposables.add(this._editor.onDidChangeModel(_ => this._createOutline())); + this._disposables.add(this._editor.onDidChangeModelLanguage(_ => this._createOutline())); + + // update soon'ish as model content change + const updateSoon = new TimeoutTimer(); + this._disposables.add(updateSoon); + this._disposables.add(this._editor.onDidChangeModelContent(event => { + const timeout = OutlineModel.getRequestDelay(this._editor!.getModel()); + updateSoon.cancelAndSet(() => this._createOutline(event), timeout); + })); + + // stop when editor dies + this._disposables.add(this._editor.onDidDispose(() => this._outlineDisposables.clear())); + + // initial load + this._createOutline().finally(() => firstLoadBarrier.open()); + } + + dispose(): void { + this._disposables.dispose(); + this._outlineDisposables.dispose(); + } + + get isEmpty(): boolean { + return !this._outlineModel || TreeElement.empty(this._outlineModel); + } + + async reveal(entry: DocumentSymbolItem, options: IEditorOptions, sideBySide: boolean): Promise { + const model = OutlineModel.get(entry); + if (!model || !(entry instanceof OutlineElement)) { + return; + } + await this._codeEditorService.openCodeEditor({ + resource: model.uri, + options: { + ...options, + selection: Range.collapseToStart(entry.symbol.selectionRange), + selectionRevealType: TextEditorSelectionRevealType.NearTopIfOutsideViewport, + } + }, this._editor, sideBySide); + } + + preview(entry: DocumentSymbolItem): IDisposable { + if (!(entry instanceof OutlineElement)) { + return Disposable.None; + } + + const { symbol } = entry; + this._editor.revealRangeInCenterIfOutsideViewport(symbol.range, ScrollType.Smooth); + const ids = this._editor.deltaDecorations([], [{ + range: symbol.range, + options: { + className: 'rangeHighlight', + isWholeLine: true + } + }]); + return toDisposable(() => this._editor.deltaDecorations(ids, [])); + } + + captureViewState(): IDisposable { + const viewState = this._editor.saveViewState(); + return toDisposable(() => { + if (viewState) { + this._editor.restoreViewState(viewState); + } + }); + } + + private async _createOutline(contentChangeEvent?: IModelContentChangedEvent): Promise { + + this._outlineDisposables.clear(); + if (!contentChangeEvent) { + this._setOutlineModel(undefined); + } + + if (!this._editor.hasModel()) { + return; + } + const buffer = this._editor.getModel(); + if (!DocumentSymbolProviderRegistry.has(buffer)) { + return; + } + + const cts = new CancellationTokenSource(); + const versionIdThen = buffer.getVersionId(); + const timeoutTimer = new TimeoutTimer(); + + this._outlineDisposables.add(timeoutTimer); + this._outlineDisposables.add(toDisposable(() => cts.dispose(true))); + + try { + let model = await OutlineModel.create(buffer, cts.token); + if (cts.token.isCancellationRequested) { + // cancelled -> do nothing + return; + } + + if (TreeElement.empty(model) || !this._editor.hasModel()) { + // empty -> no outline elements + this._setOutlineModel(model); + return; + } + + // heuristic: when the symbols-to-lines ratio changes by 50% between edits + // wait a little (and hope that the next change isn't as drastic). + if (contentChangeEvent && this._outlineModel && buffer.getLineCount() >= 25) { + const newSize = TreeElement.size(model); + const newLength = buffer.getValueLength(); + const newRatio = newSize / newLength; + const oldSize = TreeElement.size(this._outlineModel); + const oldLength = newLength - contentChangeEvent.changes.reduce((prev, value) => prev + value.rangeLength, 0); + const oldRatio = oldSize / oldLength; + if (newRatio <= oldRatio * 0.5 || newRatio >= oldRatio * 1.5) { + // wait for a better state and ignore current model when more + // typing has happened + const value = await raceCancellation(timeout(2000).then(() => true), cts.token, false); + if (!value) { + return; + } + } + } + + // copy the model + model = model.adopt(); + + // feature: show markers with outline element + this._applyMarkersToOutline(model); + this._outlineDisposables.add(this._markerDecorationsService.onDidChangeMarker(textModel => { + if (isEqual(model.uri, textModel.uri)) { + this._applyMarkersToOutline(model); + this._onDidChange.fire({}); + } + })); + this._outlineDisposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) { + if (this._configurationService.getValue(OutlineConfigKeys.problemsEnabled)) { + this._applyMarkersToOutline(model); + } else { + model.updateMarker([]); + } + this._onDidChange.fire({}); + } + if (e.affectsConfiguration('outline')) { + // outline filtering, problems on/off + this._onDidChange.fire({}); + } + if (e.affectsConfiguration('breadcrumbs') && this._editor.hasModel()) { + // breadcrumbs filtering + this._breadcrumbsDataSource.update(model, this._editor.getPosition()); + this._onDidChange.fire({}); + } + })); + + // feature: toggle icons + this._outlineDisposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(OutlineConfigKeys.icons)) { + this._onDidChange.fire({}); + } + if (e.affectsConfiguration('outline')) { + this._onDidChange.fire({}); + } + })); + + // feature: update active when cursor changes + this._outlineDisposables.add(this._editor.onDidChangeCursorPosition(_ => { + timeoutTimer.cancelAndSet(() => { + if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId() && this._editor.hasModel()) { + this._breadcrumbsDataSource.update(model, this._editor.getPosition()); + this._onDidChange.fire({ affectOnlyActiveElement: true }); + } + }, 150); + })); + + // update properties, send event + this._setOutlineModel(model); + + } catch (err) { + this._setOutlineModel(undefined); + onUnexpectedError(err); + } + } + + private _applyMarkersToOutline(model: OutlineModel | undefined): void { + if (!model || !this._configurationService.getValue(OutlineConfigKeys.problemsEnabled)) { + return; + } + const markers: IOutlineMarker[] = []; + for (const [range, marker] of this._markerDecorationsService.getLiveMarkers(model.uri)) { + if (marker.severity === MarkerSeverity.Error || marker.severity === MarkerSeverity.Warning) { + markers.push({ ...range, severity: marker.severity }); + } + } + model.updateMarker(markers); + } + + private _setOutlineModel(model: OutlineModel | undefined) { + const position = this._editor.getPosition(); + if (!position || !model) { + this._outlineModel = undefined; + this._breadcrumbsDataSource.clear(); + } else { + if (!this._outlineModel?.merge(model)) { + this._outlineModel = model; + } + this._breadcrumbsDataSource.update(model, position); + } + this._onDidChange.fire({}); + } +} + +class DocumentSymbolsOutlineCreator implements IOutlineCreator { + + readonly dispose: () => void; + + constructor( + @IOutlineService outlineService: IOutlineService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + const reg = outlineService.registerOutlineCreator(this); + this.dispose = () => reg.dispose(); + } + + matches(candidate: IEditorPane): candidate is IEditorPane { + const ctrl = candidate.getControl(); + return isCodeEditor(ctrl) || isDiffEditor(ctrl); + } + + async createOutline(pane: IEditorPane, target: OutlineTarget, _token: CancellationToken): Promise | undefined> { + const control = pane.getControl(); + let editor: ICodeEditor | undefined; + if (isCodeEditor(control)) { + editor = control; + } else if (isDiffEditor(control)) { + editor = control.getModifiedEditor(); + } + if (!editor) { + return undefined; + } + const firstLoadBarrier = new Barrier(); + const result = this._instantiationService.createInstance(DocumentSymbolsOutline, editor, target, firstLoadBarrier); + await firstLoadBarrier.wait(); + return result; + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DocumentSymbolsOutlineCreator, LifecyclePhase.Eventually); diff --git a/src/vs/editor/contrib/documentSymbols/media/outlineTree.css b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.css similarity index 77% rename from src/vs/editor/contrib/documentSymbols/media/outlineTree.css rename to src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.css index ccdfe57a4..107c29992 100644 --- a/src/vs/editor/contrib/documentSymbols/media/outlineTree.css +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.css @@ -20,11 +20,7 @@ color: var(--outline-element-color); } -.monaco-list .outline-element .monaco-icon-label-container .monaco-highlighted-label, -.monaco-list .outline-element .monaco-icon-label-container .label-description { - white-space: nowrap; -} - +.monaco-breadcrumbs .outline-element .outline-element-decoration, .monaco-list .outline-element .outline-element-decoration { opacity: 0.75; font-size: 90%; @@ -35,10 +31,17 @@ color: var(--outline-element-color); } +/* when showing in breadcrumbs than hide a few things, like markers or descriptions */ +.monaco-breadcrumbs .outline-element .monaco-icon-label-container .monaco-icon-description-container, +.monaco-breadcrumbs .outline-element .outline-element-decoration { + display: none; +} + .monaco-list .outline-element .outline-element-decoration.bubble { font-family: codicon; font-size: 14px; opacity: 0.4; + padding-right: 8px; } .monaco-list .outline-element .outline-element-icon { diff --git a/src/vs/editor/contrib/documentSymbols/outlineTree.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts similarity index 82% rename from src/vs/editor/contrib/documentSymbols/outlineTree.ts rename to src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts index c0ef55263..64b463a09 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineTree.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts @@ -3,35 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./documentSymbolsTree'; import * as dom from 'vs/base/browser/dom'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IDataSource, ITreeNode, ITreeRenderer, ITreeSorter, ITreeFilter } from 'vs/base/browser/ui/tree/tree'; +import { ITreeNode, ITreeRenderer, ITreeFilter } from 'vs/base/browser/ui/tree/tree'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; -import 'vs/css!./media/outlineTree'; -import 'vs/css!./media/symbol-icons'; import { Range } from 'vs/editor/common/core/range'; import { SymbolKind, SymbolKinds, SymbolTag } from 'vs/editor/common/modes'; import { OutlineElement, OutlineGroup, OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { localize } from 'vs/nls'; -import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { OutlineConfigKeys } from 'vs/editor/contrib/documentSymbols/outline'; import { MarkerSeverity } from 'vs/platform/markers/common/markers'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { registerColor, listErrorForeground, listWarningForeground, foreground } from 'vs/platform/theme/common/colorRegistry'; import { IdleValue } from 'vs/base/common/async'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { URI } from 'vs/base/common/uri'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { Iterable } from 'vs/base/common/iterator'; import { Codicon } from 'vs/base/common/codicons'; +import { IOutlineComparator, OutlineConfigKeys } from 'vs/workbench/services/outline/browser/outline'; -export type OutlineItem = OutlineGroup | OutlineElement; +export type DocumentSymbolItem = OutlineGroup | OutlineElement; -export class OutlineNavigationLabelProvider implements IKeyboardNavigationLabelProvider { +export class DocumentSymbolNavigationLabelProvider implements IKeyboardNavigationLabelProvider { - getKeyboardNavigationLabel(element: OutlineItem): { toString(): string; } { + getKeyboardNavigationLabel(element: DocumentSymbolItem): { toString(): string; } { if (element instanceof OutlineGroup) { return element.label; } else { @@ -40,15 +37,14 @@ export class OutlineNavigationLabelProvider implements IKeyboardNavigationLabelP } } -export class OutlineAccessibilityProvider implements IListAccessibilityProvider { +export class DocumentSymbolAccessibilityProvider implements IListAccessibilityProvider { - constructor(private readonly ariaLabel: string) { } + constructor(private readonly _ariaLabel: string) { } getWidgetAriaLabel(): string { - return this.ariaLabel; + return this._ariaLabel; } - - getAriaLabel(element: OutlineItem): string | null { + getAriaLabel(element: DocumentSymbolItem): string | null { if (element instanceof OutlineGroup) { return element.label; } else { @@ -57,22 +53,22 @@ export class OutlineAccessibilityProvider implements IListAccessibilityProvider< } } -export class OutlineIdentityProvider implements IIdentityProvider { - getId(element: OutlineItem): { toString(): string; } { +export class DocumentSymbolIdentityProvider implements IIdentityProvider { + getId(element: DocumentSymbolItem): { toString(): string; } { return element.id; } } -export class OutlineGroupTemplate { - static readonly id = 'OutlineGroupTemplate'; +class DocumentSymbolGroupTemplate { + static readonly id = 'DocumentSymbolGroupTemplate'; constructor( readonly labelContainer: HTMLElement, readonly label: HighlightedLabel, ) { } } -export class OutlineElementTemplate { - static readonly id = 'OutlineElementTemplate'; +class DocumentSymbolTemplate { + static readonly id = 'DocumentSymbolTemplate'; constructor( readonly container: HTMLElement, readonly iconLabel: IconLabel, @@ -81,70 +77,66 @@ export class OutlineElementTemplate { ) { } } -export class OutlineVirtualDelegate implements IListVirtualDelegate { +export class DocumentSymbolVirtualDelegate implements IListVirtualDelegate { - getHeight(_element: OutlineItem): number { + getHeight(_element: DocumentSymbolItem): number { return 22; } - getTemplateId(element: OutlineItem): string { - if (element instanceof OutlineGroup) { - return OutlineGroupTemplate.id; - } else { - return OutlineElementTemplate.id; - } + getTemplateId(element: DocumentSymbolItem): string { + return element instanceof OutlineGroup + ? DocumentSymbolGroupTemplate.id + : DocumentSymbolTemplate.id; } } -export class OutlineGroupRenderer implements ITreeRenderer { +export class DocumentSymbolGroupRenderer implements ITreeRenderer { - readonly templateId: string = OutlineGroupTemplate.id; + readonly templateId: string = DocumentSymbolGroupTemplate.id; - renderTemplate(container: HTMLElement): OutlineGroupTemplate { + renderTemplate(container: HTMLElement): DocumentSymbolGroupTemplate { const labelContainer = dom.$('.outline-element-label'); container.classList.add('outline-element'); dom.append(container, labelContainer); - return new OutlineGroupTemplate(labelContainer, new HighlightedLabel(labelContainer, true)); + return new DocumentSymbolGroupTemplate(labelContainer, new HighlightedLabel(labelContainer, true)); } - renderElement(node: ITreeNode, index: number, template: OutlineGroupTemplate): void { - template.label.set( - node.element.label, - createMatches(node.filterData) - ); + renderElement(node: ITreeNode, _index: number, template: DocumentSymbolGroupTemplate): void { + template.label.set(node.element.label, createMatches(node.filterData)); } - disposeTemplate(_template: OutlineGroupTemplate): void { + disposeTemplate(_template: DocumentSymbolGroupTemplate): void { // nothing } } -export class OutlineElementRenderer implements ITreeRenderer { +export class DocumentSymbolRenderer implements ITreeRenderer { - readonly templateId: string = OutlineElementTemplate.id; + readonly templateId: string = DocumentSymbolTemplate.id; constructor( + private _renderMarker: boolean, @IConfigurationService private readonly _configurationService: IConfigurationService, @IThemeService private readonly _themeService: IThemeService, ) { } - renderTemplate(container: HTMLElement): OutlineElementTemplate { + renderTemplate(container: HTMLElement): DocumentSymbolTemplate { container.classList.add('outline-element'); const iconLabel = new IconLabel(container, { supportHighlights: true }); const iconClass = dom.$('.outline-element-icon'); const decoration = dom.$('.outline-element-decoration'); container.prepend(iconClass); container.appendChild(decoration); - return new OutlineElementTemplate(container, iconLabel, iconClass, decoration); + return new DocumentSymbolTemplate(container, iconLabel, iconClass, decoration); } - renderElement(node: ITreeNode, index: number, template: OutlineElementTemplate): void { + renderElement(node: ITreeNode, _index: number, template: DocumentSymbolTemplate): void { const { element } = node; - const options = { + const options: IIconLabelValueOptions = { matches: createMatches(node.filterData), labelEscapeNewLines: true, - extraClasses: [], - title: localize('title.template', "{0} ({1})", element.symbol.name, OutlineElementRenderer._symbolKindNames[element.symbol.kind]) + extraClasses: ['nowrap'], + title: localize('title.template', "{0} ({1})", element.symbol.name, DocumentSymbolRenderer._symbolKindNames[element.symbol.kind]) }; if (this._configurationService.getValue(OutlineConfigKeys.icons)) { // add styles for the icons @@ -152,14 +144,17 @@ export class OutlineElementRenderer implements ITreeRenderer= 0) { - options.extraClasses.push(`deprecated`); + options.extraClasses!.push(`deprecated`); options.matches = []; } template.iconLabel.setLabel(element.symbol.name, element.symbol.detail, options); - this._renderMarkerInfo(element, template); + + if (this._renderMarker) { + this._renderMarkerInfo(element, template); + } } - private _renderMarkerInfo(element: OutlineElement, template: OutlineElementTemplate): void { + private _renderMarkerInfo(element: OutlineElement, template: DocumentSymbolTemplate): void { if (!element.marker) { dom.hide(template.decoration); @@ -227,47 +222,12 @@ export class OutlineElementRenderer implements ITreeRenderer { - - static readonly configNameToKind = Object.freeze({ - ['showFiles']: SymbolKind.File, - ['showModules']: SymbolKind.Module, - ['showNamespaces']: SymbolKind.Namespace, - ['showPackages']: SymbolKind.Package, - ['showClasses']: SymbolKind.Class, - ['showMethods']: SymbolKind.Method, - ['showProperties']: SymbolKind.Property, - ['showFields']: SymbolKind.Field, - ['showConstructors']: SymbolKind.Constructor, - ['showEnums']: SymbolKind.Enum, - ['showInterfaces']: SymbolKind.Interface, - ['showFunctions']: SymbolKind.Function, - ['showVariables']: SymbolKind.Variable, - ['showConstants']: SymbolKind.Constant, - ['showStrings']: SymbolKind.String, - ['showNumbers']: SymbolKind.Number, - ['showBooleans']: SymbolKind.Boolean, - ['showArrays']: SymbolKind.Array, - ['showObjects']: SymbolKind.Object, - ['showKeys']: SymbolKind.Key, - ['showNull']: SymbolKind.Null, - ['showEnumMembers']: SymbolKind.EnumMember, - ['showStructs']: SymbolKind.Struct, - ['showEvents']: SymbolKind.Event, - ['showOperators']: SymbolKind.Operator, - ['showTypeParameters']: SymbolKind.TypeParameter, - }); +export class DocumentSymbolFilter implements ITreeFilter { static readonly kindToConfigName = Object.freeze({ [SymbolKind.File]: 'showFiles', @@ -299,60 +259,48 @@ export class OutlineFilter implements ITreeFilter { }); constructor( - private readonly _prefix: string, + private readonly _prefix: 'breadcrumbs' | 'outline', @ITextResourceConfigurationService private readonly _textResourceConfigService: ITextResourceConfigurationService, ) { } - filter(element: OutlineItem): boolean { + filter(element: DocumentSymbolItem): boolean { const outline = OutlineModel.get(element); - let uri: URI | undefined; - - if (outline) { - uri = outline.uri; - } - if (!(element instanceof OutlineElement)) { return true; } - - const configName = OutlineFilter.kindToConfigName[element.symbol.kind]; + const configName = DocumentSymbolFilter.kindToConfigName[element.symbol.kind]; const configKey = `${this._prefix}.${configName}`; - return this._textResourceConfigService.getValue(uri, configKey); + return this._textResourceConfigService.getValue(outline?.uri, configKey); } } -export class OutlineItemComparator implements ITreeSorter { +export class DocumentSymbolComparator implements IOutlineComparator { private readonly _collator = new IdleValue(() => new Intl.Collator(undefined, { numeric: true })); - constructor( - public type: OutlineSortOrder = OutlineSortOrder.ByPosition - ) { } - - compare(a: OutlineItem, b: OutlineItem): number { + compareByPosition(a: DocumentSymbolItem, b: DocumentSymbolItem): number { if (a instanceof OutlineGroup && b instanceof OutlineGroup) { return a.order - b.order; - } else if (a instanceof OutlineElement && b instanceof OutlineElement) { - if (this.type === OutlineSortOrder.ByKind) { - return a.symbol.kind - b.symbol.kind || this._collator.value.compare(a.symbol.name, b.symbol.name); - } else if (this.type === OutlineSortOrder.ByName) { - return this._collator.value.compare(a.symbol.name, b.symbol.name) || Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range); - } else if (this.type === OutlineSortOrder.ByPosition) { - return Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range) || this._collator.value.compare(a.symbol.name, b.symbol.name); - } + return Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range) || this._collator.value.compare(a.symbol.name, b.symbol.name); } return 0; } -} - -export class OutlineDataSource implements IDataSource { - - getChildren(element: undefined | OutlineModel | OutlineGroup | OutlineElement) { - if (!element) { - return Iterable.empty(); + compareByType(a: DocumentSymbolItem, b: DocumentSymbolItem): number { + if (a instanceof OutlineGroup && b instanceof OutlineGroup) { + return a.order - b.order; + } else if (a instanceof OutlineElement && b instanceof OutlineElement) { + return a.symbol.kind - b.symbol.kind || this._collator.value.compare(a.symbol.name, b.symbol.name); } - return element.children.values(); + return 0; + } + compareByName(a: DocumentSymbolItem, b: DocumentSymbolItem): number { + if (a instanceof OutlineGroup && b instanceof OutlineGroup) { + return a.order - b.order; + } else if (a instanceof OutlineElement && b instanceof OutlineElement) { + return this._collator.value.compare(a.symbol.name, b.symbol.name) || Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range); + } + return 0; } } @@ -721,5 +669,4 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = if (symbolIconVariableColor) { collector.addRule(`${Codicon.symbolVariable.cssSelector} { color: ${symbolIconVariableColor}; }`); } - }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index eb4566740..99417655a 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -30,10 +30,10 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv } private get configuration() { - const editorConfig = this.configurationService.getValue().workbench.editor; + const editorConfig = this.configurationService.getValue().workbench?.editor; return { - openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + openEditorPinned: !editorConfig?.enablePreviewFromQuickOpen || !editorConfig?.enablePreview }; } @@ -44,12 +44,12 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv protected gotoLocation(context: IQuickAccessTextEditorContext, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean, preserveFocus?: boolean }): void { // Check for sideBySide use - if ((options.keyMods.ctrlCmd || options.forceSideBySide) && this.editorService.activeEditor) { + if ((options.keyMods.alt || (this.configuration.openEditorPinned && options.keyMods.ctrlCmd) || options.forceSideBySide) && this.editorService.activeEditor) { context.restoreViewState?.(); // since we open to the side, restore view state in this editor this.editorService.openEditor(this.editorService.activeEditor, { selection: options.range, - pinned: options.keyMods.alt || this.configuration.openEditorPinned, + pinned: options.keyMods.ctrlCmd || this.configuration.openEditorPinned, preserveFocus: options.preserveFocus }, SIDE_GROUP); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index bb7642fe7..4f76c5547 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -12,9 +12,9 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions as QuickaccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from 'vs/editor/contrib/quickAccess/gotoSymbolQuickAccess'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkbenchEditorConfiguration, IEditorPane } from 'vs/workbench/common/editor'; +import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { ITextModel } from 'vs/editor/common/model'; -import { DisposableStore, IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable, Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; @@ -23,10 +23,11 @@ import { prepareQuery } from 'vs/base/common/fuzzyScorer'; import { SymbolKind } from 'vs/editor/common/modes'; import { fuzzyScore, createMatches } from 'vs/base/common/filters'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; +import { IOutlineService, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; +import { isCompositeEditor } from 'vs/editor/browser/editorBrowser'; export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider { @@ -34,7 +35,8 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess constructor( @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IOutlineService private readonly outlineService: IOutlineService, ) { super({ openSideBySideDirection: () => this.configuration.openSideBySideDirection @@ -43,36 +45,37 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess //#region DocumentSymbols (text editor required) - protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken): IDisposable { - if (this.canPickFromTableOfContents()) { - return this.doGetTableOfContentsPicks(picker); - } - - return super.provideWithTextEditor(context, picker, token); - } - private get configuration() { - const editorConfig = this.configurationService.getValue().workbench.editor; + const editorConfig = this.configurationService.getValue().workbench?.editor; return { - openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, - openSideBySideDirection: editorConfig.openSideBySideDirection + openEditorPinned: !editorConfig?.enablePreviewFromQuickOpen || !editorConfig?.enablePreview, + openSideBySideDirection: editorConfig?.openSideBySideDirection }; } protected get activeTextEditorControl() { + // TODO@bpasero this distinction should go away by adopting `IOutlineService` + // for all editors (either text based ones or not). Currently text based + // editors are not yet using the new outline service infrastructure but the + // "classical" document symbols approach. + + if (isCompositeEditor(this.editorService.activeEditorPane?.getControl())) { + return undefined; + } + return this.editorService.activeTextEditorControl; } protected gotoLocation(context: IQuickAccessTextEditorContext, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean, preserveFocus?: boolean }): void { // Check for sideBySide use - if ((options.keyMods.ctrlCmd || options.forceSideBySide) && this.editorService.activeEditor) { + if ((options.keyMods.alt || (this.configuration.openEditorPinned && options.keyMods.ctrlCmd) || options.forceSideBySide) && this.editorService.activeEditor) { context.restoreViewState?.(); // since we open to the side, restore view state in this editor this.editorService.openEditor(this.editorService.activeEditor, { selection: options.range, - pinned: options.keyMods.alt || this.configuration.openEditorPinned, + pinned: options.keyMods.ctrlCmd || this.configuration.openEditorPinned, preserveFocus: options.preserveFocus }, SIDE_GROUP); } @@ -104,7 +107,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess return []; } - return this.doGetSymbolPicks(this.getDocumentSymbols(model, true, token), prepareQuery(filter), options, token); + return this.doGetSymbolPicks(this.getDocumentSymbols(model, token), prepareQuery(filter), options, token); } addDecorations(editor: IEditor, range: IRange): void { @@ -118,22 +121,21 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess //#endregion protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { - if (this.canPickFromTableOfContents()) { - return this.doGetTableOfContentsPicks(picker); + if (this.canPickWithOutlineService()) { + return this.doGetOutlinePicks(picker); } return super.provideWithoutTextEditor(picker); } - private canPickFromTableOfContents(): boolean { - return this.editorService.activeEditorPane ? TableOfContentsProviderRegistry.has(this.editorService.activeEditorPane.getId()) : false; + private canPickWithOutlineService(): boolean { + return this.editorService.activeEditorPane ? this.outlineService.canCreateOutline(this.editorService.activeEditorPane) : false; } - private doGetTableOfContentsPicks(picker: IQuickPick): IDisposable { + private doGetOutlinePicks(picker: IQuickPick): IDisposable { const pane = this.editorService.activeEditorPane; if (!pane) { return Disposable.None; } - const provider = TableOfContentsProviderRegistry.get(pane.getId())!; const cts = new CancellationTokenSource(); const disposables = new DisposableStore(); @@ -141,30 +143,45 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess picker.busy = true; - provider.provideTableOfContents(pane, { disposables }, cts.token).then(entries => { + this.outlineService.createOutline(pane, OutlineTarget.QuickPick, cts.token).then(outline => { - picker.busy = false; - - if (cts.token.isCancellationRequested || !entries || entries.length === 0) { + if (!outline) { return; } + if (cts.token.isCancellationRequested) { + outline.dispose(); + return; + } + + disposables.add(outline); + + const viewState = outline.captureViewState(); + disposables.add(toDisposable(() => { + if (picker.selectedItems.length === 0) { + viewState.dispose(); + } + })); + + const entries = Array.from(outline.config.quickPickDataSource.getQuickPickElements()); const items: IGotoSymbolQuickPickItem[] = entries.map((entry, idx) => { return { kind: SymbolKind.File, index: idx, score: 0, - label: entry.icon ? `$(${entry.icon.id}) ${entry.label}` : entry.label, - ariaLabel: entry.detail ? `${entry.label}, ${entry.detail}` : entry.label, - detail: entry.detail, + label: entry.label, description: entry.description, + ariaLabel: entry.ariaLabel, + iconClasses: entry.iconClasses }; }); disposables.add(picker.onDidAccept(() => { picker.hide(); const [entry] = picker.selectedItems; - entries[entry.index]?.pick(); + if (entry && entries[entry.index]) { + outline.reveal(entries[entry.index].element, {}, false); + } })); const updatePickerItems = () => { @@ -194,16 +211,23 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess updatePickerItems(); disposables.add(picker.onDidChangeValue(updatePickerItems)); + const previewDisposable = new MutableDisposable(); + disposables.add(previewDisposable); + disposables.add(picker.onDidChangeActive(() => { const [entry] = picker.activeItems; - if (entry) { - entries[entry.index]?.preview(); + if (entry && entries[entry.index]) { + previewDisposable.value = outline.preview(entries[entry.index].element); + } else { + previewDisposable.clear(); } })); }).catch(err => { onUnexpectedError(err); picker.hide(); + }).finally(() => { + picker.busy = false; }); return disposables; @@ -243,45 +267,3 @@ registerAction2(class GotoSymbolAction extends Action2 { accessor.get(IQuickInputService).quickAccess.show(GotoSymbolQuickAccessProvider.PREFIX); } }); - -//#region toc definition and logic - -export interface ITableOfContentsEntry { - icon?: ThemeIcon; - label: string; - detail?: string; - description?: string; - pick(): any; - preview(): any; -} - -export interface ITableOfContentsProvider { - - provideTableOfContents(editor: T, context: { disposables: DisposableStore }, token: CancellationToken): Promise; -} - -class ProviderRegistry { - - private readonly _provider = new Map(); - - register(type: string, provider: ITableOfContentsProvider): IDisposable { - this._provider.set(type, provider); - return toDisposable(() => { - if (this._provider.get(type) === provider) { - this._provider.delete(type); - } - }); - } - - get(type: string): ITableOfContentsProvider | undefined { - return this._provider.get(type); - } - - has(type: string): boolean { - return this._provider.has(type); - } -} - -export const TableOfContentsProviderRegistry = new ProviderRegistry(); - -//#endregion diff --git a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts index f7e084d03..4e693565f 100644 --- a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts @@ -42,25 +42,25 @@ suite('Save Participants', function () { let lineContent = ''; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), lineContent); + assert.strictEqual(snapshotToString(model.createSnapshot()!), lineContent); // No new line if last line already empty lineContent = `Hello New Line${model.textEditorModel.getEOL()}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), lineContent); + assert.strictEqual(snapshotToString(model.createSnapshot()!), lineContent); // New empty line added (single line) lineContent = 'Hello New Line'; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), `${lineContent}${model.textEditorModel.getEOL()}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${lineContent}${model.textEditorModel.getEOL()}`); // New empty line added (multi line) lineContent = `Hello New Line${model.textEditorModel.getEOL()}Hello New Line${model.textEditorModel.getEOL()}Hello New Line`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), `${lineContent}${model.textEditorModel.getEOL()}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${lineContent}${model.textEditorModel.getEOL()}`); }); test('trim final new lines', async function () { @@ -77,25 +77,25 @@ suite('Save Participants', function () { let lineContent = `${textContent}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), lineContent); + assert.strictEqual(snapshotToString(model.createSnapshot()!), lineContent); // No new line removal if last line is single new line lineContent = `${textContent}${eol}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), lineContent); + assert.strictEqual(snapshotToString(model.createSnapshot()!), lineContent); // Remove new line (single line with two new lines) lineContent = `${textContent}${eol}${eol}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); // Remove new lines (multiple lines with multiple new lines) lineContent = `${textContent}${eol}${textContent}${eol}${eol}${eol}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}${textContent}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}${textContent}${eol}`); }); test('trim final new lines bug#39750', async function () { @@ -117,12 +117,12 @@ suite('Save Participants', function () { // undo await model.textEditorModel.undo(); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}`); // trim final new lines should not mess the undo stack await participant.participate(model, { reason: SaveReason.EXPLICIT }); await model.textEditorModel.redo(); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}.`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}.`); }); test('trim final new lines bug#46075', async function () { @@ -143,13 +143,13 @@ suite('Save Participants', function () { } // confirm trimming - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); // undo should go back to previous content immediately await model.textEditorModel.undo(); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}${eol}`); await model.textEditorModel.redo(); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); }); test('trim whitespace', async function () { @@ -169,6 +169,6 @@ suite('Save Participants', function () { } // confirm trimming - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}`); }); }); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index ffab7e07c..1d4e0e380 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -25,7 +25,7 @@ import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { peekViewBorder } from 'vs/editor/contrib/peekView/peekView'; import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget'; import * as nls from 'vs/nls'; -import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -45,7 +45,6 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; -import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { Codicon } from 'vs/base/common/codicons'; @@ -239,15 +238,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget const actionsContainer = dom.append(this._headElement, dom.$('.review-actions')); this._actionbarWidget = new ActionBar(actionsContainer, { - actionViewItemProvider: (action: IAction) => { - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } else { - return new ActionViewItem({}, action, { label: false, icon: true }); - } - } + actionViewItemProvider: createActionViewItem.bind(undefined, this.instantiationService) }); this._disposables.add(this._actionbarWidget); diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 9ddb42f76..0249effb3 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -142,6 +142,7 @@ export class CommentNodeRenderer implements IListRenderer } templateData.commentText.appendChild(renderedComment); + templateData.commentText.title = renderedComment.textContent ?? ''; } disposeTemplate(templateData: ICommentThreadTemplateData): void { diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 5ed011a2c..ec5544fd8 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -6,11 +6,9 @@ import 'vs/css!./media/panel'; import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { basename, isEqual } from 'vs/base/common/resources'; -import { IAction, Action } from 'vs/base/common/actions'; -import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; +import { basename } from 'vs/base/common/resources'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CommentNode, CommentsModel, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { CommentController } from 'vs/workbench/contrib/comments/browser/commentsEditorContribution'; @@ -20,14 +18,19 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; import { ResourceLabels } from 'vs/workbench/browser/labels'; import { CommentsList, COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyAndExpr, ContextKeyEqualsExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; + +const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey('commentsView.hasComments', false); export class CommentsPanel extends ViewPane { private treeLabels!: ResourceLabels; @@ -35,7 +38,7 @@ export class CommentsPanel extends ViewPane { private treeContainer!: HTMLElement; private messageBoxContainer!: HTMLElement; private commentsModel!: CommentsModel; - private collapseAllAction?: IAction; + private readonly hasCommentsContextKey: IContextKey; readonly onDidChangeVisibility = this.onDidChangeBodyVisibility; @@ -52,8 +55,10 @@ export class CommentsPanel extends ViewPane { @IThemeService themeService: IThemeService, @ICommentService private readonly commentService: ICommentService, @ITelemetryService telemetryService: ITelemetryService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.hasCommentsContextKey = CONTEXT_KEY_HAS_COMMENTS.bindTo(contextKeyService); } public renderBody(container: HTMLElement): void { @@ -129,13 +134,14 @@ export class CommentsPanel extends ViewPane { await this.tree.setInput(this.commentsModel); } - public getActions(): IAction[] { - if (!this.collapseAllAction) { - this.collapseAllAction = new Action('vs.tree.collapse', nls.localize('collapseAll', "Collapse All"), 'collapse-all', true, () => this.tree ? new CollapseAllAction(this.tree, true).run() : Promise.resolve()); - this._register(this.collapseAllAction); + public collapseAll() { + if (this.tree) { + this.tree.collapseAll(); + this.tree.setSelection([]); + this.tree.setFocus([]); + this.tree.domFocus(); + this.tree.focusFirst(); } - - return [this.collapseAllAction]; } public layoutBody(height: number, width: number): void { @@ -206,7 +212,7 @@ export class CommentsPanel extends ViewPane { const activeEditor = this.editorService.activeEditor; let currentActiveResource = activeEditor ? activeEditor.resource : undefined; - if (currentActiveResource && isEqual(currentActiveResource, element.resource)) { + if (this.uriIdentityService.extUri.isEqual(element.resource, currentActiveResource)) { const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId; const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment.uniqueIdInThread : element.comment.uniqueIdInThread; const control = this.editorService.activeTextEditorControl; @@ -243,9 +249,7 @@ export class CommentsPanel extends ViewPane { private async refresh(): Promise { if (this.isVisible()) { - if (this.collapseAllAction) { - this.collapseAllAction.enabled = this.commentsModel.hasCommentThreads(); - } + this.hasCommentsContextKey.set(this.commentsModel.hasCommentThreads()); this.treeContainer.classList.toggle('hidden', !this.commentsModel.hasCommentThreads()); this.renderMessage(); @@ -281,3 +285,23 @@ CommandsRegistry.registerCommand({ viewsService.openView(COMMENTS_VIEW_ID, true); } }); + +registerAction2(class Collapse extends ViewAction { + constructor() { + super({ + viewId: COMMENTS_VIEW_ID, + id: 'comments.collapse', + title: nls.localize('collapseAll', "Collapse All"), + f1: false, + icon: Codicon.collapseAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('view', COMMENTS_VIEW_ID), CONTEXT_KEY_HAS_COMMENTS]) + } + }); + } + runInView(_accessor: ServicesAccessor, view: CommentsPanel) { + view.collapseAll(); + } +}); diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index c57aa6dd3..66de7e83c 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce, distinct } from 'vs/base/common/arrays'; +import { coalesce, distinct, firstOrDefault } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { Lazy } from 'vs/base/common/lazy'; @@ -28,7 +28,7 @@ import { EditorInput, EditorOptions, Extensions as EditorInputExtensions, GroupI import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { CONTEXT_CUSTOM_EDITORS, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CustomEditorCapabilities, CustomEditorInfo, CustomEditorInfoCollection, CustomEditorPriority, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; -import { IWebviewService, webviewHasOwnEditFunctionsContext } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CustomEditorAssociation, CustomEditorsAssociations, customEditorsAssociationsSettingId } from 'vs/workbench/services/editor/common/editorOpenWith'; import { ICustomEditorInfo, ICustomEditorViewTypesHandler, IEditorService, IOpenEditorOverride, IOpenEditorOverrideEntry } from 'vs/workbench/services/editor/common/editorService'; @@ -45,7 +45,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ private readonly _customEditorContextKey: IContextKey; private readonly _focusedCustomEditorIsEditable: IContextKey; - private readonly _webviewHasOwnEditFunctions: IContextKey; private readonly _onDidChangeViewTypes = new Emitter(); onDidChangeViewTypes: Event = this._onDidChangeViewTypes.event; @@ -66,7 +65,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._customEditorContextKey = CONTEXT_CUSTOM_EDITORS.bindTo(contextKeyService); this._focusedCustomEditorIsEditable = CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE.bindTo(contextKeyService); - this._webviewHasOwnEditFunctions = webviewHasOwnEditFunctionsContext.bindTo(contextKeyService); this._contributedEditors = this._register(new ContributedCustomEditors(storageService)); this._register(this._contributedEditors.onChange(() => { @@ -156,13 +154,8 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ ...this.getAllCustomEditors(resource).allEditors, ]); - let currentlyOpenedEditorType: undefined | string; - for (const editor of group ? group.editors : []) { - if (editor.resource && isEqual(editor.resource, resource)) { - currentlyOpenedEditorType = editor instanceof CustomEditorInput ? editor.viewType : defaultCustomEditor.id; - break; - } - } + const existingEditorForResource = group && firstOrDefault(this.editorService.findEditors(resource, group)); + const currentlyOpenedEditorType: undefined | string = existingEditorForResource instanceof CustomEditorInput ? existingEditorForResource.viewType : defaultCustomEditor.id; const resourceExt = extname(resource); @@ -278,9 +271,8 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ } // Try to replace existing editors for resource - const existingEditors = targetGroup.editors.filter(editor => editor.resource && isEqual(editor.resource, resource)); - if (existingEditors.length) { - const existing = existingEditors[0]; + const existing = firstOrDefault(this.editorService.findEditors(resource, targetGroup)); + if (existing) { if (!input.matches(existing)) { await this.editorService.replaceEditors([{ editor: existing, @@ -317,7 +309,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ if (!resource) { this._customEditorContextKey.reset(); this._focusedCustomEditorIsEditable.reset(); - this._webviewHasOwnEditFunctions.reset(); return; } @@ -325,7 +316,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._customEditorContextKey.set(possibleEditors.map(x => x.id).join(',')); this._focusedCustomEditorIsEditable.set(activeEditorPane?.input instanceof CustomEditorInput); - this._webviewHasOwnEditFunctions.set(possibleEditors.length > 0); } private async handleMovedFileInOpenedFileEditors(oldResource: URI, newResource: URI): Promise { @@ -457,7 +447,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo return this.onEditorOpening(editor, options, group); }, getEditorOverrides: (resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined): IOpenEditorOverrideEntry[] => { - const currentEditor = group?.editors.find(editor => isEqual(editor.resource, resource)); + const currentEditor = group && firstOrDefault(this.editorService.findEditors(resource, group)); const toOverride = (entry: CustomEditorInfo): IOpenEditorOverrideEntry => { return { @@ -546,7 +536,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo return; } - const existingEditorForResource = group.editors.find(editor => isEqual(resource, editor.resource)); + const existingEditorForResource = firstOrDefault(this.editorService.findEditors(resource, group)); if (existingEditorForResource) { if (editor === existingEditorForResource) { return; diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 966dc83e5..65d81d074 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -15,7 +15,6 @@ import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness, import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { RemoveBreakpointAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, BreakpointWidgetContext, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugConfiguration, State, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -173,6 +172,24 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi this.setDecorationsScheduler.schedule(); } + /** + * Returns context menu actions at the line number if breakpoints can be + * set. This is used by the {@link TestingDecorations} to allow breakpoint + * setting on lines where breakpoint "run" actions are present. + */ + public getContextMenuActionsAtPosition(lineNumber: number, model: ITextModel) { + if (!this.debugService.getAdapterManager().hasDebuggers()) { + return []; + } + + if (!this.debugService.canSetBreakpointsIn(model)) { + return []; + } + + const breakpoints = this.debugService.getModel().getBreakpoints({ lineNumber, uri: model.uri }); + return this.getContextMenuActions(breakpoints, model.uri, lineNumber); + } + private registerListeners(): void { this.toDispose.push(this.editor.onMouseDown(async (e: IEditorMouseEvent) => { if (!this.debugService.getAdapterManager().hasDebuggers()) { @@ -301,7 +318,9 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi const actions: IAction[] = []; if (breakpoints.length === 1) { const breakpointType = breakpoints[0].logMessage ? nls.localize('logPoint', "Logpoint") : nls.localize('breakpoint', "Breakpoint"); - actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, nls.localize('removeBreakpoint', "Remove {0}", breakpointType), this.debugService)); + actions.push(new Action('debug.removeBreakpoint', nls.localize('removeBreakpoint', "Remove {0}", breakpointType), undefined, true, async () => { + await this.debugService.removeBreakpoints(breakpoints[0].getId()); + })); actions.push(new Action( 'workbench.debug.action.editBreakpointAction', nls.localize('editBreakpoint', "Edit {0}...", breakpointType), @@ -375,7 +394,8 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi const decorations = this.editor.getLineDecorations(line); if (decorations) { for (const { options } of decorations) { - if (options.glyphMarginClassName && options.glyphMarginClassName.indexOf('codicon-') === -1) { + const clz = options.glyphMarginClassName; + if (clz && (!clz.includes('codicon-') || clz.includes('codicon-testing-'))) { return false; } } @@ -454,7 +474,7 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi // Candidate decoration has a breakpoint attached when a breakpoint is already at that location and we did not yet set a decoration there // In practice this happens for the first breakpoint that was set on a line // We could have also rendered this first decoration as part of desiredBreakpointDecorations however at that moment we have no location information - const icon = candidate.breakpoint ? getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), candidate.breakpoint, this.labelService).icon : icons.debugBreakpointDisabled; + const icon = candidate.breakpoint ? getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), candidate.breakpoint, this.labelService).icon : icons.breakpoint.disabled; const contextMenuActions = () => this.getContextMenuActions(candidate.breakpoint ? [candidate.breakpoint] : [], activeCodeEditor.getModel().uri, candidate.range.startLineNumber, candidate.range.startColumn); const inlineWidget = new InlineBreakpointWidget(activeCodeEditor, decorationId, ThemeIcon.asClassName(icon), candidate.breakpoint, this.debugService, this.contextMenuService, contextMenuActions); @@ -645,15 +665,11 @@ registerThemingParticipant((theme, collector) => { const debugIconBreakpointColor = theme.getColor(debugIconBreakpointForeground); if (debugIconBreakpointColor) { collector.addRule(` - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpoint)}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointConditional)}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointLog)}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointFunction)}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointData)}, + ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.regular)}`).join(',\n ')}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointUnsupported)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointHint)}:not([class*='codicon-debug-breakpoint']):not([class*='codicon-debug-stackframe']), - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpoint)}${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)}::after, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpoint)}${ThemeIcon.asCSSSelector(icons.debugStackframe)}::after { + .monaco-workbench ${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)}::after, + .monaco-workbench ${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframe)}::after { color: ${debugIconBreakpointColor} !important; } `); @@ -662,7 +678,7 @@ registerThemingParticipant((theme, collector) => { const debugIconBreakpointDisabledColor = theme.getColor(debugIconBreakpointDisabledForeground); if (debugIconBreakpointDisabledColor) { collector.addRule(` - .monaco-workbench .codicon[class*='-disabled'] { + ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.disabled)}`).join(',\n ')} { color: ${debugIconBreakpointDisabledColor} !important; } `); @@ -671,7 +687,7 @@ registerThemingParticipant((theme, collector) => { const debugIconBreakpointUnverifiedColor = theme.getColor(debugIconBreakpointUnverifiedForeground); if (debugIconBreakpointUnverifiedColor) { collector.addRule(` - .monaco-workbench .codicon[class*='-unverified'] { + ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.unverified)}`).join(',\n ')} { color: ${debugIconBreakpointUnverifiedColor}; } `); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 362d05447..cc8b39633 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -221,6 +221,9 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.input = scopedInstatiationService.createInstance(CodeEditorWidget, container, options, codeEditorWidgetOptions); CONTEXT_IN_BREAKPOINT_WIDGET.bindTo(scopedContextKeyService).set(true); const model = this.modelService.createModel('', null, uri.parse(`${DEBUG_SCHEME}:${this.editor.getId()}:breakpointinput`), true); + if (this.editor.hasModel()) { + model.setMode(this.editor.getModel().getLanguageIdentifier()); + } this.input.setModel(model); this.toDispose.push(model); const setDecorations = () => { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 7fa72c43f..4fb3fead1 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -3,13 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as resources from 'vs/base/common/resources'; import * as dom from 'vs/base/browser/dom'; -import { IAction, Action, Separator } from 'vs/base/common/actions'; -import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINTS_FOCUSED, State, DEBUG_SCHEME, IFunctionBreakpoint, IExceptionBreakpoint, IEnablement, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, IDebugModel, IDataBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; +import { IAction } from 'vs/base/common/actions'; +import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINTS_FOCUSED, State, DEBUG_SCHEME, IFunctionBreakpoint, IExceptionBreakpoint, IEnablement, IDebugModel, IDataBreakpoint, BREAKPOINTS_VIEW_ID, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, IBaseBreakpoint, IBreakpointEditorContribution, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINT_INPUT_FOCUSED } from 'vs/workbench/contrib/debug/common/debug'; import { ExceptionBreakpoint, FunctionBreakpoint, Breakpoint, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; -import { AddFunctionBreakpointAction, ToggleBreakpointsActivatedAction, RemoveAllBreakpointsAction, RemoveBreakpointAction, EnableAllBreakpointsAction, DisableAllBreakpointsAction, ReapplyBreakpointsAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -24,12 +22,11 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { WorkbenchList, ListResourceNavigator } from 'vs/platform/list/browser/listService'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; -import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyEqualsExpr, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Gesture } from 'vs/base/browser/touch'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; @@ -38,6 +35,13 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { registerAction2, Action2, MenuId, IMenu, IMenuService } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { createAndFillInContextMenuActions, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Codicon } from 'vs/base/common/codicons'; const $ = dom.$; @@ -57,11 +61,21 @@ export function getExpandedBodySize(model: IDebugModel, countLimit: number): num } type BreakpointItem = IBreakpoint | IFunctionBreakpoint | IDataBreakpoint | IExceptionBreakpoint; +interface InputBoxData { + breakpoint: IFunctionBreakpoint | IExceptionBreakpoint; + type: 'condition' | 'hitCount' | 'name'; +} + export class BreakpointsView extends ViewPane { private list!: WorkbenchList; private needsRefresh = false; private ignoreLayout = false; + private menu: IMenu; + private breakpointItemType: IContextKey; + private breakpointSupportsCondition: IContextKey; + private _inputBoxData: InputBoxData | undefined; + breakpointInputFocused: IContextKey; constructor( options: IViewletViewOptions, @@ -78,26 +92,32 @@ export class BreakpointsView extends ViewPane { @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, @ILabelService private readonly labelService: ILabelService, + @IMenuService menuService: IMenuService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.menu = menuService.createMenu(MenuId.DebugBreakpointsContext, contextKeyService); + this._register(this.menu); + this.breakpointItemType = CONTEXT_BREAKPOINT_ITEM_TYPE.bindTo(contextKeyService); + this.breakpointSupportsCondition = CONTEXT_BREAKPOINT_SUPPORTS_CONDITION.bindTo(contextKeyService); + this.breakpointInputFocused = CONTEXT_BREAKPOINT_INPUT_FOCUSED.bindTo(contextKeyService); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); } - public renderBody(container: HTMLElement): void { + renderBody(container: HTMLElement): void { super.renderBody(container); this.element.classList.add('debug-pane'); container.classList.add('debug-breakpoints'); - const delegate = new BreakpointsDelegate(this.debugService); + const delegate = new BreakpointsDelegate(this); this.list = >this.instantiationService.createInstance(WorkbenchList, 'Breakpoints', container, delegate, [ - this.instantiationService.createInstance(BreakpointsRenderer), - new ExceptionBreakpointsRenderer(this.debugService), - new ExceptionBreakpointInputRenderer(this.debugService, this.contextViewService, this.themeService), - this.instantiationService.createInstance(FunctionBreakpointsRenderer), + this.instantiationService.createInstance(BreakpointsRenderer, this.menu, this.breakpointSupportsCondition), + new ExceptionBreakpointsRenderer(this.menu, this.breakpointSupportsCondition, this.debugService), + new ExceptionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.themeService), + this.instantiationService.createInstance(FunctionBreakpointsRenderer, this.menu, this.breakpointSupportsCondition), this.instantiationService.createInstance(DataBreakpointsRenderer), - new FunctionBreakpointInputRenderer(this.debugService, this.contextViewService, this.themeService, this.labelService) + new FunctionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.themeService, this.labelService) ], { identityProvider: { getId: (element: IEnablement) => element.getId() }, multipleSelectionSupport: false, @@ -135,10 +155,9 @@ export class BreakpointsView extends ViewPane { if (e.element instanceof Breakpoint) { openBreakpointSource(e.element, e.sideBySide, e.editorOptions.preserveFocus || false, e.editorOptions.pinned || !e.editorOptions.preserveFocus, this.debugService, this.editorService); } - if (e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2 && e.element instanceof FunctionBreakpoint && e.element !== this.debugService.getViewModel().getSelectedBreakpoint()) { + if (e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2 && e.element instanceof FunctionBreakpoint && e.element !== this.inputBoxData?.breakpoint) { // double click - this.debugService.getViewModel().setSelectedBreakpoint(e.element); - this.onBreakpointsChange(); + this.renderInputBox({ breakpoint: e.element, type: 'name' }); } })); @@ -156,13 +175,23 @@ export class BreakpointsView extends ViewPane { })); } - public focus(): void { + focus(): void { super.focus(); if (this.list) { this.list.domFocus(); } } + renderInputBox(data: InputBoxData | undefined): void { + this._inputBoxData = data; + this.onBreakpointsChange(); + this._inputBoxData = undefined; + } + + get inputBoxData(): InputBoxData | undefined { + return this._inputBoxData; + } + protected layoutBody(height: number, width: number): void { if (this.ignoreLayout) { return; @@ -181,71 +210,25 @@ export class BreakpointsView extends ViewPane { } private onListContextMenu(e: IListContextMenuEvent): void { - if (!e.element) { - return; - } - - const actions: IAction[] = []; const element = e.element; + const type = element instanceof Breakpoint ? 'breakpoint' : element instanceof ExceptionBreakpoint ? 'exceptionBreakpoint' : + element instanceof FunctionBreakpoint ? 'functionBreakpoint' : element instanceof DataBreakpoint ? 'dataBreakpoint' : undefined; + this.breakpointItemType.set(type); + const session = this.debugService.getViewModel().focusedSession; + const conditionSupported = element instanceof ExceptionBreakpoint ? element.supportsCondition : (!session || !!session.capabilities.supportsConditionalBreakpoints); + this.breakpointSupportsCondition.set(conditionSupported); - if (element instanceof ExceptionBreakpoint) { - if (element.supportsCondition) { - actions.push(new Action('workbench.action.debug.editExceptionBreakpointCondition', nls.localize('editCondition', "Edit Condition"), '', true, async () => { - this.debugService.getViewModel().setSelectedBreakpoint(element); - this.onBreakpointsChange(); - })); - } - } else { - const breakpointType = element instanceof Breakpoint && element.logMessage ? nls.localize('Logpoint', "Logpoint") : nls.localize('Breakpoint', "Breakpoint"); - if (element instanceof Breakpoint || element instanceof FunctionBreakpoint) { - actions.push(new Action('workbench.action.debug.openEditorAndEditBreakpoint', nls.localize('editBreakpoint', "Edit {0}...", breakpointType), '', true, async () => { - if (element instanceof Breakpoint) { - const editor = await openBreakpointSource(element, false, false, true, this.debugService, this.editorService); - if (editor) { - const codeEditor = editor.getControl(); - if (isCodeEditor(codeEditor)) { - codeEditor.getContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID).showBreakpointWidget(element.lineNumber, element.column); - } - } - } else { - this.debugService.getViewModel().setSelectedBreakpoint(element); - this.onBreakpointsChange(); - } - })); - actions.push(new Separator()); - } - - - actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, nls.localize('removeBreakpoint', "Remove {0}", breakpointType), this.debugService)); - - if (this.debugService.getModel().getBreakpoints().length + this.debugService.getModel().getFunctionBreakpoints().length >= 1) { - actions.push(new RemoveAllBreakpointsAction(RemoveAllBreakpointsAction.ID, RemoveAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); - actions.push(new Separator()); - - actions.push(new EnableAllBreakpointsAction(EnableAllBreakpointsAction.ID, EnableAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); - actions.push(new DisableAllBreakpointsAction(DisableAllBreakpointsAction.ID, DisableAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); - } - - actions.push(new Separator()); - actions.push(new ReapplyBreakpointsAction(ReapplyBreakpointsAction.ID, ReapplyBreakpointsAction.LABEL, this.debugService, this.keybindingService)); - } + const secondary: IAction[] = []; + const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: e.element, shouldForwardArgs: false }, { primary: [], secondary }, g => /^inline/.test(g)); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, - getActions: () => actions, + getActions: () => secondary, getActionsContext: () => element, - onHide: () => dispose(actions) + onHide: () => dispose(actionsDisposable) }); } - public getActions(): IAction[] { - return [ - new AddFunctionBreakpointAction(AddFunctionBreakpointAction.ID, AddFunctionBreakpointAction.LABEL, this.debugService, this.keybindingService), - new ToggleBreakpointsActivatedAction(ToggleBreakpointsActivatedAction.ID, ToggleBreakpointsActivatedAction.ACTIVATE_LABEL, this.debugService, this.keybindingService), - new RemoveAllBreakpointsAction(RemoveAllBreakpointsAction.ID, RemoveAllBreakpointsAction.LABEL, this.debugService, this.keybindingService) - ]; - } - private updateSize(): void { const containerModel = this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!; @@ -282,7 +265,7 @@ export class BreakpointsView extends ViewPane { class BreakpointsDelegate implements IListVirtualDelegate { - constructor(private debugService: IDebugService) { + constructor(private view: BreakpointsView) { // noop } @@ -295,16 +278,16 @@ class BreakpointsDelegate implements IListVirtualDelegate { return BreakpointsRenderer.ID; } if (element instanceof FunctionBreakpoint) { - const selected = this.debugService.getViewModel().getSelectedBreakpoint(); - if (!element.name || (selected && selected.getId() === element.getId())) { + const inputBoxBreakpoint = this.view.inputBoxData?.breakpoint; + if (!element.name || (inputBoxBreakpoint && inputBoxBreakpoint.getId() === element.getId())) { return FunctionBreakpointInputRenderer.ID; } return FunctionBreakpointsRenderer.ID; } if (element instanceof ExceptionBreakpoint) { - const selected = this.debugService.getViewModel().getSelectedBreakpoint(); - if (selected && selected.getId() === element.getId()) { + const inputBoxBreakpoint = this.view.inputBoxData?.breakpoint; + if (inputBoxBreakpoint && inputBoxBreakpoint.getId() === element.getId()) { return ExceptionBreakpointInputRenderer.ID; } return ExceptionBreakpointsRenderer.ID; @@ -322,7 +305,9 @@ interface IBaseBreakpointTemplateData { name: HTMLElement; checkbox: HTMLInputElement; context: BreakpointItem; + actionBar: ActionBar; toDispose: IDisposable[]; + elementDisposable: IDisposable[]; } interface IBaseBreakpointWithIconTemplateData extends IBaseBreakpointTemplateData { @@ -338,26 +323,31 @@ interface IExceptionBreakpointTemplateData extends IBaseBreakpointTemplateData { condition: HTMLElement; } +interface IFunctionBreakpointTemplateData extends IBaseBreakpointWithIconTemplateData { + condition: HTMLElement; +} + interface IFunctionBreakpointInputTemplateData { inputBox: InputBox; checkbox: HTMLInputElement; icon: HTMLElement; breakpoint: IFunctionBreakpoint; - reactedOnEvent: boolean; toDispose: IDisposable[]; + type: 'hitCount' | 'condition' | 'name'; } interface IExceptionBreakpointInputTemplateData { inputBox: InputBox; checkbox: HTMLInputElement; breakpoint: IExceptionBreakpoint; - reactedOnEvent: boolean; toDispose: IDisposable[]; } class BreakpointsRenderer implements IListRenderer { constructor( + private menu: IMenu, + private breakpointSupportsCondition: IContextKey, @IDebugService private readonly debugService: IDebugService, @ILabelService private readonly labelService: ILabelService ) { @@ -377,6 +367,7 @@ class BreakpointsRenderer implements IListRenderer { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -387,6 +378,8 @@ class BreakpointsRenderer implements IListRenderer /^inline/.test(g))); + data.actionBar.clear(); + data.actionBar.push(primary, { icon: true, label: false }); + } + + disposeElement(_element: IBreakpoint, _index: number, templateData: IBreakpointTemplateData): void { + dispose(templateData.elementDisposable); } disposeTemplate(templateData: IBreakpointTemplateData): void { @@ -423,6 +427,8 @@ class BreakpointsRenderer implements IListRenderer { constructor( + private menu: IMenu, + private breakpointSupportsCondition: IContextKey, private debugService: IDebugService ) { // noop @@ -440,6 +446,7 @@ class ExceptionBreakpointsRenderer implements IListRenderer { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -450,6 +457,8 @@ class ExceptionBreakpointsRenderer implements IListRenderer /^inline/.test(g))); + data.actionBar.clear(); + data.actionBar.push(primary, { icon: true, label: false }); + } + + disposeElement(_element: IExceptionBreakpoint, _index: number, templateData: IExceptionBreakpointTemplateData): void { + dispose(templateData.elementDisposable); } disposeTemplate(templateData: IExceptionBreakpointTemplateData): void { @@ -467,9 +486,11 @@ class ExceptionBreakpointsRenderer implements IListRenderer { +class FunctionBreakpointsRenderer implements IListRenderer { constructor( + private menu: IMenu, + private breakpointSupportsCondition: IContextKey, @IDebugService private readonly debugService: IDebugService, @ILabelService private readonly labelService: ILabelService ) { @@ -482,13 +503,14 @@ class FunctionBreakpointsRenderer implements IListRenderer { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -497,11 +519,15 @@ class FunctionBreakpointsRenderer implements IListRenderer /^inline/.test(g))); + data.actionBar.clear(); + data.actionBar.push(primary, { icon: true, label: false }); } - disposeTemplate(templateData: IBaseBreakpointWithIconTemplateData): void { + disposeElement(_element: IFunctionBreakpoint, _index: number, templateData: IFunctionBreakpointTemplateData): void { + dispose(templateData.elementDisposable); + } + + disposeTemplate(templateData: IFunctionBreakpointTemplateData): void { dispose(templateData.toDispose); } } @@ -570,7 +611,7 @@ class DataBreakpointsRenderer implements IListRenderer { constructor( + private view: BreakpointsView, private debugService: IDebugService, private contextViewService: IContextViewService, private themeService: IThemeService, private labelService: ILabelService - ) { - // noop - } + ) { } static readonly ID = 'functionbreakpointinput'; @@ -605,22 +645,33 @@ class FunctionBreakpointInputRenderer implements IListRenderer { - if (!template.reactedOnEvent) { - template.reactedOnEvent = true; - this.debugService.getViewModel().setSelectedBreakpoint(undefined); - if (inputBox.value && (renamed || template.breakpoint.name)) { - this.debugService.renameFunctionBreakpoint(template.breakpoint.getId(), renamed ? inputBox.value : template.breakpoint.name); + const wrapUp = (success: boolean) => { + this.view.breakpointInputFocused.set(false); + const id = template.breakpoint.getId(); + + if (success) { + if (template.type === 'name') { + this.debugService.updateFunctionBreakpoint(id, { name: inputBox.value }); + } + if (template.type === 'condition') { + this.debugService.updateFunctionBreakpoint(id, { condition: inputBox.value }); + } + if (template.type === 'hitCount') { + this.debugService.updateFunctionBreakpoint(id, { hitCondition: inputBox.value }); + } + } else { + if (template.type === 'name' && !template.breakpoint.name) { + this.debugService.removeFunctionBreakpoints(id); } else { - this.debugService.removeFunctionBreakpoints(template.breakpoint.getId()); + this.view.renderInputBox(undefined); } } }; @@ -650,7 +701,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer { data.inputBox.focus(); data.inputBox.select(); @@ -672,6 +738,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer { constructor( + private view: BreakpointsView, private debugService: IDebugService, private contextViewService: IContextViewService, private themeService: IThemeService @@ -693,24 +760,22 @@ class ExceptionBreakpointInputRenderer implements IListRenderer { - if (!template.reactedOnEvent) { - template.reactedOnEvent = true; - this.debugService.getViewModel().setSelectedBreakpoint(undefined); - let newCondition = template.breakpoint.condition; - if (success) { - newCondition = inputBox.value !== '' ? inputBox.value : undefined; - } - this.debugService.setExceptionBreakpointCondition(template.breakpoint, newCondition); + this.view.breakpointInputFocused.set(false); + let newCondition = template.breakpoint.condition; + if (success) { + newCondition = inputBox.value !== '' ? inputBox.value : undefined; } + this.debugService.setExceptionBreakpointCondition(template.breakpoint, newCondition); }; toDispose.push(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => { @@ -736,7 +801,6 @@ class ExceptionBreakpointInputRenderer implements IListRenderer { + const debugService = accessor.get(IDebugService); + if (breakpoint instanceof Breakpoint) { + await debugService.removeBreakpoints(breakpoint.getId()); + } else if (breakpoint instanceof FunctionBreakpoint) { + await debugService.removeFunctionBreakpoints(breakpoint.getId()); + } else if (breakpoint instanceof DataBreakpoint) { + await debugService.removeDataBreakpoints(breakpoint.getId()); + } + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.removeAllBreakpoints', + title: { + original: 'Remove All Breakpoints', + value: localize('removeAllBreakpoints', "Remove All Breakpoints"), + mnemonicTitle: localize({ key: 'miRemoveAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Remove &&All Breakpoints") + }, + f1: true, + icon: icons.breakpointsRemoveAll, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 30, + when: ContextKeyEqualsExpr.create('view', BREAKPOINTS_VIEW_ID) + }, { + id: MenuId.DebugBreakpointsContext, + group: '3_modification', + order: 20, + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINT_ITEM_TYPE.notEqualsTo('exceptionBreakpoint')) + }, { + id: MenuId.MenubarDebugMenu, + group: '5_breakpoints', + order: 3, + when: CONTEXT_DEBUGGERS_AVAILABLE + }] + }); + } + + run(accessor: ServicesAccessor): void { + const debugService = accessor.get(IDebugService); + debugService.removeBreakpoints(); + debugService.removeFunctionBreakpoints(); + debugService.removeDataBreakpoints(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.enableAllBreakpoints', + title: { + original: '', + value: localize('enableAllBreakpoints', "Enable All Breakpoints"), + mnemonicTitle: localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "&&Enable All Breakpoints"), + }, + f1: true, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'z_commands', + order: 10, + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINT_ITEM_TYPE.notEqualsTo('exceptionBreakpoint')) + }, { + id: MenuId.MenubarDebugMenu, + group: '5_breakpoints', + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const debugService = accessor.get(IDebugService); + await debugService.enableOrDisableBreakpoints(true); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.disableAllBreakpoints', + title: { + original: 'Disable All Breakpoints', + value: localize('disableAllBreakpoints', "Disable All Breakpoints"), + mnemonicTitle: localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints") + }, + f1: true, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'z_commands', + order: 20, + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINT_ITEM_TYPE.notEqualsTo('exceptionBreakpoint')) + }, { + id: MenuId.MenubarDebugMenu, + group: '5_breakpoints', + order: 2, + + when: CONTEXT_DEBUGGERS_AVAILABLE + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const debugService = accessor.get(IDebugService); + await debugService.enableOrDisableBreakpoints(false); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.reapplyBreakpointsAction', + title: localize('reapplyAllBreakpoints', "Reapply All Breakpoints"), + f1: true, + precondition: CONTEXT_IN_DEBUG_MODE, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'z_commands', + order: 30, + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINT_ITEM_TYPE.notEqualsTo('exceptionBreakpoint')) + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const debugService = accessor.get(IDebugService); + await debugService.setBreakpointsActivated(true); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.editBreakpoint', + viewId: BREAKPOINTS_VIEW_ID, + title: localize('editCondition', "Edit Condition..."), + icon: Codicon.edit, + precondition: CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'navigation', + order: 10 + }, { + id: MenuId.DebugBreakpointsContext, + group: 'inline', + order: 10 + }] + }); + } + + async runInView(accessor: ServicesAccessor, view: BreakpointsView, breakpoint: ExceptionBreakpoint | Breakpoint | FunctionBreakpoint): Promise { + const debugService = accessor.get(IDebugService); + const editorService = accessor.get(IEditorService); + if (breakpoint instanceof Breakpoint) { + const editor = await openBreakpointSource(breakpoint, false, false, true, debugService, editorService); + if (editor) { + const codeEditor = editor.getControl(); + if (isCodeEditor(codeEditor)) { + codeEditor.getContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID).showBreakpointWidget(breakpoint.lineNumber, breakpoint.column); + } + } + } else { + view.renderInputBox({ breakpoint, type: 'condition' }); + } + } +}); + + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.editFunctionBreakpoint', + viewId: BREAKPOINTS_VIEW_ID, + title: localize('editBreakpoint', "Edit Function Breakpoint..."), + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: '1_breakpoints', + order: 10, + when: CONTEXT_BREAKPOINT_ITEM_TYPE.isEqualTo('functionBreakpoint') + }] + }); + } + + runInView(_accessor: ServicesAccessor, view: BreakpointsView, breakpoint: IFunctionBreakpoint) { + view.renderInputBox({ breakpoint, type: 'name' }); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.editFunctionBreakpointHitCount', + viewId: BREAKPOINTS_VIEW_ID, + title: localize('editHitCount', "Edit Hit Count..."), + precondition: CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'navigation', + order: 20, + when: CONTEXT_BREAKPOINT_ITEM_TYPE.isEqualTo('functionBreakpoint') + }] + }); + } + + runInView(_accessor: ServicesAccessor, view: BreakpointsView, breakpoint: IFunctionBreakpoint) { + view.renderInputBox({ breakpoint, type: 'hitCount' }); + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index 9fd5d78b0..fa7901ea3 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -127,17 +127,21 @@ export class CallStackEditorContribution implements IEditorContribution { const isSessionFocused = s === focusedStackFrame?.thread.session; s.getAllThreads().forEach(t => { if (t.stopped) { - let candidateStackFrame = t === focusedStackFrame?.thread ? focusedStackFrame : undefined; - if (!candidateStackFrame) { - const callStack = t.getCallStack(); - if (callStack.length) { - candidateStackFrame = callStack[0]; + const callStack = t.getCallStack(); + const stackFrames: IStackFrame[] = []; + if (callStack.length > 0) { + // Always decorate top stack frame, and decorate focused stack frame if it is not the top stack frame + if (focusedStackFrame && !focusedStackFrame.equals(callStack[0])) { + stackFrames.push(focusedStackFrame); } + stackFrames.push(callStack[0]); } - if (candidateStackFrame && this.uriIdentityService.extUri.isEqual(candidateStackFrame.source.uri, this.editor.getModel()?.uri)) { - decorations.push(...createDecorationsForStackFrame(candidateStackFrame, this.topStackFrameRange, isSessionFocused)); - } + stackFrames.forEach(candidateStackFrame => { + if (candidateStackFrame && this.uriIdentityService.extUri.isEqual(candidateStackFrame.source.uri, this.editor.getModel()?.uri)) { + decorations.push(...createDecorationsForStackFrame(candidateStackFrame, this.topStackFrameRange, isSessionFocused)); + } + }); } }); }); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 70851479d..1fc41463c 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -3,22 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, State, IStackFrame, IDebugSession, IThread, CONTEXT_CALLSTACK_ITEM_TYPE, IDebugModel } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, State, IStackFrame, IDebugSession, IThread, CONTEXT_CALLSTACK_ITEM_TYPE, IDebugModel, CALLSTACK_VIEW_ID, CONTEXT_DEBUG_STATE, getStateLabel } from 'vs/workbench/contrib/debug/common/debug'; import { Thread, StackFrame, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MenuId, IMenu, IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { MenuId, IMenu, IMenuService, MenuItemAction, SubmenuItemAction, registerAction2 } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IAction, Action } from 'vs/base/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IContextKey, IContextKeyService, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ILabelService } from 'vs/platform/label/common/label'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { createAndFillInContextMenuActions, createAndFillInActionBarActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -33,7 +32,6 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; import { STOP_ID, STOP_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, RESTART_SESSION_ID, RESTART_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STEP_INTO_LABEL, STEP_INTO_ID, STEP_OUT_LABEL, STEP_OUT_ID, PAUSE_ID, PAUSE_LABEL, CONTINUE_ID, CONTINUE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -47,6 +45,8 @@ import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree' import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { localize } from 'vs/nls'; +import { Codicon } from 'vs/base/common/codicons'; const $ = dom.$; @@ -162,7 +162,7 @@ export class CallStackView extends ViewPane { this.stateMessageLabel.classList.toggle('exception', thread.stoppedDetails.reason === 'exception'); this.stateMessage.hidden = false; } else if (sessions.length === 1 && sessions[0].state === State.Running) { - this.stateMessageLabel.textContent = nls.localize({ key: 'running', comment: ['indicates state'] }, "Running"); + this.stateMessageLabel.textContent = localize({ key: 'running', comment: ['indicates state'] }, "Running"); this.stateMessageLabel.title = sessions[0].getLabel(); this.stateMessageLabel.classList.remove('exception'); this.stateMessage.hidden = false; @@ -205,14 +205,6 @@ export class CallStackView extends ViewPane { this.stateMessageLabel = dom.append(this.stateMessage, $('span.label')); } - getActions(): IAction[] { - if (this.stateMessage.hidden) { - return [new CollapseAction(() => this.tree, true, 'explorer-action ' + ThemeIcon.asClassName(icons.debugCollapseAll))]; - } - - return []; - } - renderBody(container: HTMLElement): void { super.renderBody(container); this.element.classList.add('debug-pane'); @@ -220,11 +212,11 @@ export class CallStackView extends ViewPane { const treeContainer = renderViewTree(container); this.dataSource = new CallStackDataSource(this.debugService); - const sessionsRenderer = this.instantiationService.createInstance(SessionsRenderer, this.menu); + const sessionsRenderer = this.instantiationService.createInstance(SessionsRenderer, this.menu, this.callStackItemType); this.tree = >this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'CallStackView', treeContainer, new CallStackDelegate(), new CallStackCompressionDelegate(this.debugService), [ sessionsRenderer, - new ThreadsRenderer(this.instantiationService), - this.instantiationService.createInstance(StackFramesRenderer), + new ThreadsRenderer(this.callStackItemType, this.instantiationService), + this.instantiationService.createInstance(StackFramesRenderer, this.callStackItemType), new ErrorsRenderer(), new LoadAllRenderer(this.themeService), new ShowMoreRenderer(this.themeService) @@ -259,7 +251,7 @@ export class CallStackView extends ViewPane { return LoadAllRenderer.LABEL; } - return nls.localize('showMoreStackFrames2', "Show More Stack Frames"); + return localize('showMoreStackFrames2', "Show More Stack Frames"); }, getCompressedNodeKeyboardNavigationLabel: (e: CallStackItem[]) => { const firstItem = e[0]; @@ -378,6 +370,10 @@ export class CallStackView extends ViewPane { this.tree.domFocus(); } + collapseAll(): void { + this.tree.collapseAll(); + } + private async updateTreeSelection(): Promise { if (!this.tree || !this.tree.getInput()) { // Tree not initialized yet @@ -493,6 +489,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer, @IInstantiationService private readonly instantiationService: IInstantiationService ) { } @@ -532,7 +529,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer t.stopped); @@ -542,6 +539,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer /^inline/.test(g))); data.actionBar.clear(); @@ -557,7 +555,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer { static readonly ID = 'thread'; - constructor(private readonly instantiationService: IInstantiationService) { } + constructor( + private callStackItemType: IContextKey, + private readonly instantiationService: IInstantiationService + ) { } get templateId(): string { return ThreadsRenderer.ID; @@ -591,11 +592,12 @@ class ThreadsRenderer implements ICompressibleTreeRenderer, index: number, data: IThreadTemplateData): void { const thread = element.element; - data.thread.title = nls.localize('thread', "Thread"); + data.thread.title = localize('thread', "Thread"); data.label.set(thread.name, createMatches(element.filterData)); data.stateLabel.textContent = thread.stateLabel; data.actionBar.clear(); + this.callStackItemType.set('thread'); const actions = getActions(this.instantiationService, thread); data.actionBar.push(actions, { icon: true, label: false }); } @@ -613,8 +615,9 @@ class StackFramesRenderer implements ICompressibleTreeRenderer, @ILabelService private readonly labelService: ILabelService, - @INotificationService private readonly notificationService: INotificationService + @INotificationService private readonly notificationService: INotificationService, ) { } get templateId(): string { @@ -659,8 +662,9 @@ class StackFramesRenderer implements ICompressibleTreeRenderer { + const action = new Action('debug.callStack.restartFrame', localize('restartFrame', "Restart Frame"), ThemeIcon.asClassName(icons.debugRestartFrame), true, async () => { try { await stackFrame.restart(); } catch (e) { @@ -710,7 +714,7 @@ class ErrorsRenderer implements ICompressibleTreeRenderer { static readonly ID = 'loadAll'; - static readonly LABEL = nls.localize('loadAllStackFrames', "Load All Stack Frames"); + static readonly LABEL = localize('loadAllStackFrames', "Load All Stack Frames"); constructor(private readonly themeService: IThemeService) { } @@ -766,9 +770,9 @@ class ShowMoreRenderer implements ICompressibleTreeRenderer, index: number, data: ILabelTemplateData): void { const stackFrames = element.element; if (stackFrames.every(sf => !!(sf.source && sf.source.origin && sf.source.origin === stackFrames[0].source.origin))) { - data.label.textContent = nls.localize('showMoreAndOrigin', "Show {0} More: {1}", stackFrames.length, stackFrames[0].source.origin); + data.label.textContent = localize('showMoreAndOrigin', "Show {0} More: {1}", stackFrames.length, stackFrames[0].source.origin); } else { - data.label.textContent = nls.localize('showMoreStackFrames', "Show {0} More Stack Frames", stackFrames.length); + data.label.textContent = localize('showMoreStackFrames', "Show {0} More Stack Frames", stackFrames.length); } } @@ -930,26 +934,26 @@ class CallStackDataSource implements IAsyncDataSource { getWidgetAriaLabel(): string { - return nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'callStackAriaLabel' }, "Debug Call Stack"); + return localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'callStackAriaLabel' }, "Debug Call Stack"); } getAriaLabel(element: CallStackItem): string { if (element instanceof Thread) { - return nls.localize({ key: 'threadAriaLabel', comment: ['Placeholders stand for the thread name and the thread state.For example "Thread 1" and "Stopped'] }, "Thread {0} {1}", element.name, element.stateLabel); + return localize({ key: 'threadAriaLabel', comment: ['Placeholders stand for the thread name and the thread state.For example "Thread 1" and "Stopped'] }, "Thread {0} {1}", element.name, element.stateLabel); } if (element instanceof StackFrame) { - return nls.localize('stackFrameAriaLabel', "Stack Frame {0}, line {1}, {2}", element.name, element.range.startLineNumber, getSpecificSourceName(element)); + return localize('stackFrameAriaLabel', "Stack Frame {0}, line {1}, {2}", element.name, element.range.startLineNumber, getSpecificSourceName(element)); } if (isDebugSession(element)) { const thread = element.getAllThreads().find(t => t.stopped); - const state = thread ? thread.stateLabel : nls.localize({ key: 'running', comment: ['indicates state'] }, "Running"); - return nls.localize({ key: 'sessionLabel', comment: ['Placeholders stand for the session name and the session state. For example "Launch Program" and "Running"'] }, "Session {0} {1}", element.getLabel(), state); + const state = thread ? thread.stateLabel : localize({ key: 'running', comment: ['indicates state'] }, "Running"); + return localize({ key: 'sessionLabel', comment: ['Placeholders stand for the session name and the session state. For example "Launch Program" and "Running"'] }, "Session {0} {1}", element.getLabel(), state); } if (typeof element === 'string') { return element; } if (element instanceof Array) { - return nls.localize('showMoreStackFrames', "Show {0} More Stack Frames", element.length); + return localize('showMoreStackFrames', "Show {0} More Stack Frames", element.length); } // element instanceof ThreadAndSessionIds @@ -1121,3 +1125,26 @@ class CallStackCompressionDelegate implements ITreeCompressionDelegate { + constructor() { + super({ + id: 'callStack.collapse', + viewId: CALLSTACK_VIEW_ID, + title: localize('collapse', "Collapse All"), + f1: false, + icon: Codicon.collapseAll, + precondition: CONTEXT_DEBUG_STATE.isEqualTo(getStateLabel(State.Stopped)), + menu: { + id: MenuId.ViewTitle, + order: 10, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', CALLSTACK_VIEW_ID) + } + }); + } + + runInView(_accessor: ServicesAccessor, view: CallStackView) { + view.collapseAll(); + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 9438ac11c..a2f0090e4 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -17,12 +17,11 @@ import { CallStackView } from 'vs/workbench/contrib/debug/browser/callStackView' import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IDebugService, VIEWLET_ID, DEBUG_PANEL_ID, CONTEXT_IN_DEBUG_MODE, INTERNAL_CONSOLE_OPTIONS_SCHEMA, - CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, + CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, getStateLabel, State, CONTEXT_WATCH_ITEM_TYPE, } from 'vs/workbench/contrib/debug/common/debug'; -import { StartAction, AddFunctionBreakpointAction, ConfigureAction, DisableAllBreakpointsAction, EnableAllBreakpointsAction, RemoveAllBreakpointsAction, RunAction, ReapplyBreakpointsAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; import { DebugService } from 'vs/workbench/contrib/debug/browser/debugService'; -import { registerCommands, ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_STACK_TRACE_ID, REVERSE_CONTINUE_ID, STEP_BACK_ID, RESTART_SESSION_ID, TERMINATE_THREAD_ID, STEP_OVER_ID, STEP_INTO_ID, STEP_OUT_ID, PAUSE_ID, DISCONNECT_ID, STOP_ID, RESTART_FRAME_ID, CONTINUE_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, RESTART_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, STEP_OUT_LABEL, PAUSE_LABEL, DISCONNECT_LABEL, STOP_LABEL, CONTINUE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; +import { registerCommands, ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_STACK_TRACE_ID, RESTART_SESSION_ID, TERMINATE_THREAD_ID, STEP_OVER_ID, STEP_INTO_ID, STEP_OUT_ID, PAUSE_ID, DISCONNECT_ID, STOP_ID, RESTART_FRAME_ID, CONTINUE_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, RESTART_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, STEP_OUT_LABEL, PAUSE_LABEL, DISCONNECT_LABEL, STOP_LABEL, CONTINUE_LABEL, DEBUG_START_LABEL, DEBUG_START_COMMAND_ID, DEBUG_RUN_LABEL, DEBUG_RUN_COMMAND_ID, EDIT_EXPRESSION_COMMAND_ID, REMOVE_EXPRESSION_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { StatusBarColorProvider } from 'vs/workbench/contrib/debug/browser/statusbarColorProvider'; import { IViewsRegistry, Extensions as ViewExtensions, IViewContainersRegistry, ViewContainerLocation, ViewContainer } from 'vs/workbench/common/views'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; @@ -32,14 +31,13 @@ import { DebugStatusContribution } from 'vs/workbench/contrib/debug/browser/debu import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { LoadedScriptsView } from 'vs/workbench/contrib/debug/browser/loadedScriptsView'; -import { ADD_LOG_POINT_ID, TOGGLE_CONDITIONAL_BREAKPOINT_ID, TOGGLE_BREAKPOINT_ID, RunToCursorAction, registerEditorActions } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; -import { WatchExpressionsView } from 'vs/workbench/contrib/debug/browser/watchExpressionsView'; +import { RunToCursorAction, registerEditorActions } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; +import { WatchExpressionsView, ADD_WATCH_LABEL, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, ADD_WATCH_ID } from 'vs/workbench/contrib/debug/browser/watchExpressionsView'; import { VariablesView, SET_VARIABLE_ID, COPY_VALUE_ID, BREAK_WHEN_VALUE_CHANGES_ID, COPY_EVALUATE_PATH_ID, ADD_TO_WATCH_ID } from 'vs/workbench/contrib/debug/browser/variablesView'; -import { ClearReplAction, Repl } from 'vs/workbench/contrib/debug/browser/repl'; +import { Repl } from 'vs/workbench/contrib/debug/browser/repl'; import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider'; import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { DebugViewPaneContainer, OpenDebugConsoleAction, OpenDebugViewletAction } from 'vs/workbench/contrib/debug/browser/debugViewlet'; +import { DebugViewPaneContainer, OpenDebugViewletAction, OPEN_REPL_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugViewlet'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { CallStackEditorContribution } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; import { BreakpointEditorContribution } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; @@ -56,7 +54,6 @@ import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; const registry = Registry.as(WorkbenchActionRegistryExtensions.WorkbenchActions); const debugCategory = nls.localize('debugCategory', "Debug"); -const runCategroy = nls.localize('runCategory', "Run"); registerWorkbenchContributions(); registerColors(); registerCommandsAndActions(); @@ -64,8 +61,6 @@ registerDebugMenu(); registerEditorActions(); registerCommands(); registerDebugPanel(); -registry.registerWorkbenchAction(SyncActionDescriptor.from(StartAction, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()), 'Debug: Start Debugging', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); -registry.registerWorkbenchAction(SyncActionDescriptor.from(RunAction, { primary: KeyMod.CtrlCmd | KeyCode.F5, mac: { primary: KeyMod.WinCtrl | KeyCode.F5 } }), 'Run: Start Without Debugging', runCategroy, CONTEXT_DEBUGGERS_AVAILABLE); registerSingleton(IDebugService, DebugService, true); registerDebugView(); @@ -102,18 +97,10 @@ function regsiterEditorContributions(): void { function registerCommandsAndActions(): void { - registry.registerWorkbenchAction(SyncActionDescriptor.from(ConfigureAction), 'Debug: Open launch.json', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(AddFunctionBreakpointAction), 'Debug: Add Function Breakpoint', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(ReapplyBreakpointsAction), 'Debug: Reapply All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(RemoveAllBreakpointsAction), 'Debug: Remove All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(EnableAllBreakpointsAction), 'Debug: Enable All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(DisableAllBreakpointsAction), 'Debug: Disable All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(SelectAndStartAction), 'Debug: Select and Start Debugging', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(ClearReplAction), 'Debug: Clear Console', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - const registerDebugCommandPaletteItem = (id: string, title: string, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { MenuRegistry.appendMenuItem(MenuId.CommandPalette, { when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, when), + group: debugCategory, command: { id, title: `Debug: ${title}`, @@ -136,33 +123,8 @@ function registerCommandsAndActions(): void { registerDebugCommandPaletteItem(JUMP_TO_CURSOR_ID, nls.localize('SetNextStatement', "Set Next Statement"), CONTEXT_JUMP_TO_CURSOR_SUPPORTED); registerDebugCommandPaletteItem(RunToCursorAction.ID, RunToCursorAction.LABEL, ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped'))); registerDebugCommandPaletteItem(TOGGLE_INLINE_BREAKPOINT_ID, nls.localize('inlineBreakpoint', "Inline Breakpoint")); - - // Debug toolbar - - const registerDebugToolBarItem = (id: string, title: string, order: number, icon: { light?: URI, dark?: URI } | ThemeIcon, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { - MenuRegistry.appendMenuItem(MenuId.DebugToolBar, { - group: 'navigation', - when, - order, - command: { - id, - title, - icon, - precondition - } - }); - }; - - registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, 10, icons.debugContinue, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, icons.debugPause, CONTEXT_DEBUG_STATE.notEqualsTo('stopped'), CONTEXT_DEBUG_STATE.isEqualTo('running')); - registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, icons.debugStop, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); - registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, icons.debugDisconnect, CONTEXT_FOCUSED_SESSION_IS_ATTACH); - registerDebugToolBarItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, icons.debugStepOver, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, icons.debugStepInto, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, icons.debugStepOut, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(RESTART_SESSION_ID, RESTART_LABEL, 60, icons.debugRestart); - registerDebugToolBarItem(STEP_BACK_ID, nls.localize('stepBackDebug', "Step Back"), 50, icons.debugStepBack, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(REVERSE_CONTINUE_ID, nls.localize('reverseContinue', "Reverse"), 60, icons.debugReverseContinue, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugCommandPaletteItem(DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing)))); + registerDebugCommandPaletteItem(DEBUG_RUN_COMMAND_ID, DEBUG_RUN_LABEL, ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing)))); // Debug callstack context menu const registerDebugViewMenuItem = (menuId: MenuId, id: string, title: string, order: number, when?: ContextKeyExpression, precondition?: ContextKeyExpression, group = 'navigation') => { @@ -194,6 +156,12 @@ function registerCommandsAndActions(): void { registerDebugViewMenuItem(MenuId.DebugVariablesContext, ADD_TO_WATCH_ID, nls.localize('addToWatchExpressions', "Add to Watch"), 100, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, undefined, 'z_commands'); registerDebugViewMenuItem(MenuId.DebugVariablesContext, BREAK_WHEN_VALUE_CHANGES_ID, nls.localize('breakWhenValueChanges', "Break When Value Changes"), 200, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, undefined, 'z_commands'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, ADD_WATCH_ID, ADD_WATCH_LABEL, 10, undefined, undefined, '3_modification'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, EDIT_EXPRESSION_COMMAND_ID, nls.localize('editWatchExpression', "Edit Expression"), 20, CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), undefined, '3_modification'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, COPY_VALUE_ID, nls.localize('copyValue', "Copy Value"), 30, ContextKeyExpr.or(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), CONTEXT_WATCH_ITEM_TYPE.isEqualTo('variable')), CONTEXT_IN_DEBUG_MODE, '3_modification'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_EXPRESSION_COMMAND_ID, nls.localize('removeWatchExpression', "Remove Expression"), 10, CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), undefined, 'z_commands'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, 20, undefined, undefined, 'z_commands'); + // Touch Bar if (isMacintosh) { @@ -210,8 +178,8 @@ function registerCommandsAndActions(): void { }); }; - registerTouchBarEntry(StartAction.ID, StartAction.LABEL, 0, CONTEXT_IN_DEBUG_MODE.toNegated(), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-tb.png', require)); - registerTouchBarEntry(RunAction.ID, RunAction.LABEL, 1, CONTEXT_IN_DEBUG_MODE.toNegated(), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-without-debugging-tb.png', require)); + registerTouchBarEntry(DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, 0, CONTEXT_IN_DEBUG_MODE.toNegated(), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-tb.png', require)); + registerTouchBarEntry(DEBUG_RUN_COMMAND_ID, DEBUG_RUN_LABEL, 1, CONTEXT_IN_DEBUG_MODE.toNegated(), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-without-debugging-tb.png', require)); registerTouchBarEntry(CONTINUE_ID, CONTINUE_LABEL, 0, CONTEXT_DEBUG_STATE.isEqualTo('stopped'), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-tb.png', require)); registerTouchBarEntry(PAUSE_ID, PAUSE_LABEL, 1, ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, ContextKeyExpr.notEquals('debugState', 'stopped')), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/pause-tb.png', require)); registerTouchBarEntry(STEP_OVER_ID, STEP_OVER_LABEL, 2, CONTEXT_IN_DEBUG_MODE, FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/stepover-tb.png', require)); @@ -234,21 +202,12 @@ function registerDebugMenu(): void { order: 4 }); - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_panels', - command: { - id: OpenDebugConsoleAction.ID, - title: nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console") - }, - order: 2 - }); - // Debug menu MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { group: '1_debug', command: { - id: StartAction.ID, + id: DEBUG_START_COMMAND_ID, title: nls.localize({ key: 'miStartDebugging', comment: ['&& denotes a mnemonic'] }, "&&Start Debugging") }, order: 1, @@ -258,7 +217,7 @@ function registerDebugMenu(): void { MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { group: '1_debug', command: { - id: RunAction.ID, + id: DEBUG_RUN_COMMAND_ID, title: nls.localize({ key: 'miRun', comment: ['&& denotes a mnemonic'] }, "Run &&Without Debugging") }, order: 2, @@ -288,15 +247,6 @@ function registerDebugMenu(): void { }); // Configuration - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '2_configuration', - command: { - id: ConfigureAction.ID, - title: nls.localize({ key: 'miOpenConfigurations', comment: ['&& denotes a mnemonic'] }, "Open &&Configurations") - }, - order: 1, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { group: '2_configuration', @@ -354,25 +304,6 @@ function registerDebugMenu(): void { }); // New Breakpoints - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '4_new_breakpoint', - command: { - id: TOGGLE_BREAKPOINT_ID, - title: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") - }, - order: 1, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { - group: '1_breakpoints', - command: { - id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, - title: nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Conditional Breakpoint...") - }, - order: 1, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { group: '1_breakpoints', @@ -384,26 +315,6 @@ function registerDebugMenu(): void { when: CONTEXT_DEBUGGERS_AVAILABLE }); - MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { - group: '1_breakpoints', - command: { - id: AddFunctionBreakpointAction.ID, - title: nls.localize({ key: 'miFunctionBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Function Breakpoint...") - }, - order: 3, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { - group: '1_breakpoints', - command: { - id: ADD_LOG_POINT_ID, - title: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Logpoint...") - }, - order: 4, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { group: '4_new_breakpoint', title: nls.localize({ key: 'miNewBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&New Breakpoint"), @@ -412,36 +323,7 @@ function registerDebugMenu(): void { when: CONTEXT_DEBUGGERS_AVAILABLE }); - // Modify Breakpoints - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: EnableAllBreakpointsAction.ID, - title: nls.localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "&&Enable All Breakpoints") - }, - order: 1, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: DisableAllBreakpointsAction.ID, - title: nls.localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints") - }, - order: 2, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: RemoveAllBreakpointsAction.ID, - title: nls.localize({ key: 'miRemoveAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Remove &&All Breakpoints") - }, - order: 3, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); + // Breakpoint actions are registered from breakpointsView.ts // Install Debuggers MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { @@ -463,7 +345,7 @@ function registerDebugPanel(): void { icon: icons.debugConsoleViewIcon, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [DEBUG_PANEL_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), storageId: DEBUG_PANEL_ID, - focusCommand: { id: OpenDebugConsoleAction.ID }, + focusCommand: { id: OPEN_REPL_COMMAND_ID }, order: 2, hideIfEmpty: true }, ViewContainerLocation.Panel); @@ -477,8 +359,6 @@ function registerDebugPanel(): void { when: CONTEXT_DEBUGGERS_AVAILABLE, ctorDescriptor: new SyncDescriptor(Repl), }], VIEW_CONTAINER); - - registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugConsoleAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y }), 'View: Debug Console', CATEGORIES.View.value, CONTEXT_DEBUGGERS_AVAILABLE); } diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index 3ab907976..b600fc994 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -11,7 +11,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { SelectBox, ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IDebugService, IDebugSession, IDebugConfiguration, IConfig, ILaunch } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IDebugSession, IDebugConfiguration, IConfig, ILaunch, State } from 'vs/workbench/contrib/debug/common/debug'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { selectBorder, selectBackground } from 'vs/platform/theme/common/colorRegistry'; @@ -76,7 +76,9 @@ export class StartDebugActionViewItem implements IActionViewItem { this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.CLICK, () => { this.start.blur(); - this.actionRunner.run(this.action, this.context); + if (this.debugService.state !== State.Initializing) { + this.actionRunner.run(this.action, this.context); + } })); this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => { @@ -93,7 +95,7 @@ export class StartDebugActionViewItem implements IActionViewItem { this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Enter)) { + if (event.equals(KeyCode.Enter) && this.debugService.state !== State.Initializing) { this.actionRunner.run(this.action, this.context); } if (event.equals(KeyCode.RightArrow)) { diff --git a/src/vs/workbench/contrib/debug/browser/debugActions.ts b/src/vs/workbench/contrib/debug/browser/debugActions.ts deleted file mode 100644 index 3c9472104..000000000 --- a/src/vs/workbench/contrib/debug/browser/debugActions.ts +++ /dev/null @@ -1,421 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as nls from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IDebugService, State, IEnablement, IBreakpoint, IDebugSession, ILaunch } from 'vs/workbench/contrib/debug/common/debug'; -import { Variable, Breakpoint, FunctionBreakpoint, Expression } from 'vs/workbench/contrib/debug/common/debugModel'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { deepClone } from 'vs/base/common/objects'; -import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; - -export abstract class AbstractDebugAction extends Action { - - constructor( - id: string, label: string, cssClass: string, - @IDebugService protected debugService: IDebugService, - @IKeybindingService protected keybindingService: IKeybindingService, - ) { - super(id, label, cssClass, false); - this._register(this.debugService.onDidChangeState(state => this.updateEnablement(state))); - - this.updateLabel(label); - this.updateEnablement(); - } - - run(_: any): Promise { - throw new Error('implement me'); - } - - get tooltip(): string { - const keybinding = this.keybindingService.lookupKeybinding(this.id); - const keybindingLabel = keybinding && keybinding.getLabel(); - - return keybindingLabel ? `${this.label} (${keybindingLabel})` : this.label; - } - - protected updateLabel(newLabel: string): void { - this.label = newLabel; - } - - protected updateEnablement(state = this.debugService.state): void { - this.enabled = this.isEnabled(state); - } - - protected isEnabled(_: State): boolean { - return true; - } -} - -export class ConfigureAction extends AbstractDebugAction { - static readonly ID = 'workbench.action.debug.configure'; - static readonly LABEL = nls.localize('openLaunchJson', "Open {0}", 'launch.json'); - - constructor(id: string, label: string, - @IDebugService debugService: IDebugService, - @IKeybindingService keybindingService: IKeybindingService, - @INotificationService private readonly notificationService: INotificationService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IQuickInputService private readonly quickInputService: IQuickInputService - ) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.debugConfigure), debugService, keybindingService); - this._register(debugService.getConfigurationManager().onDidSelectConfiguration(() => this.updateClass())); - this.updateClass(); - } - - get tooltip(): string { - if (this.debugService.getConfigurationManager().selectedConfiguration.name) { - return ConfigureAction.LABEL; - } - - return nls.localize('launchJsonNeedsConfigurtion', "Configure or Fix 'launch.json'"); - } - - private updateClass(): void { - const configurationManager = this.debugService.getConfigurationManager(); - this.class = configurationManager.selectedConfiguration.name ? 'debug-action' + ThemeIcon.asClassName(icons.debugConfigure) : 'debug-action ' + ThemeIcon.asClassName(icons.debugConfigure) + ' notification'; - } - - async run(): Promise { - if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY || this.contextService.getWorkspace().folders.length === 0) { - this.notificationService.info(nls.localize('noFolderDebugConfig', "Please first open a folder in order to do advanced debug configuration.")); - return; - } - - const configurationManager = this.debugService.getConfigurationManager(); - let launch: ILaunch | undefined; - if (configurationManager.selectedConfiguration.name) { - launch = configurationManager.selectedConfiguration.launch; - } else { - const launches = configurationManager.getLaunches().filter(l => !l.hidden); - if (launches.length === 1) { - launch = launches[0]; - } else { - const picks = launches.map(l => ({ label: l.name, launch: l })); - const picked = await this.quickInputService.pick<{ label: string, launch: ILaunch }>(picks, { - activeItem: picks[0], - placeHolder: nls.localize({ key: 'selectWorkspaceFolder', comment: ['User picks a workspace folder or a workspace configuration file here. Workspace configuration files can contain settings and thus a launch.json configuration can be written into one.'] }, "Select a workspace folder to create a launch.json file in or add it to the workspace config file") - }); - if (picked) { - launch = picked.launch; - } - } - } - - if (launch) { - return launch.openConfigFile(false); - } - } -} - -export class StartAction extends AbstractDebugAction { - static ID = 'workbench.action.debug.start'; - static LABEL = nls.localize('startDebug', "Start Debugging"); - - constructor(id: string, label: string, - @IDebugService debugService: IDebugService, - @IKeybindingService keybindingService: IKeybindingService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - ) { - super(id, label, 'debug-action start', debugService, keybindingService); - - this._register(this.debugService.getConfigurationManager().onDidSelectConfiguration(() => this.updateEnablement())); - this._register(this.debugService.onDidNewSession(() => this.updateEnablement())); - this._register(this.debugService.onDidEndSession(() => this.updateEnablement())); - this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateEnablement())); - } - - async run(): Promise { - let { launch, name, getConfig } = this.debugService.getConfigurationManager().selectedConfiguration; - const config = await getConfig(); - const clonedConfig = deepClone(config); - return this.debugService.startDebugging(launch, clonedConfig || name, { noDebug: this.isNoDebug() }); - } - - protected isNoDebug(): boolean { - return false; - } - - static isEnabled(debugService: IDebugService) { - const sessions = debugService.getModel().getSessions(); - - if (debugService.state === State.Initializing) { - return false; - } - let { name, launch } = debugService.getConfigurationManager().selectedConfiguration; - let nameToStart = name; - - if (sessions.some(s => s.configuration.name === nameToStart && s.root === launch?.workspace)) { - // There is already a debug session running and we do not have any launch configuration selected - return false; - } - - return true; - } - - // Disabled if the launch drop down shows the launch config that is already running. - protected isEnabled(): boolean { - return StartAction.isEnabled(this.debugService); - } -} - -export class RunAction extends StartAction { - static readonly ID = 'workbench.action.debug.run'; - static LABEL = nls.localize('startWithoutDebugging', "Start Without Debugging"); - - protected isNoDebug(): boolean { - return true; - } -} - -export class SelectAndStartAction extends AbstractDebugAction { - static readonly ID = 'workbench.action.debug.selectandstart'; - static readonly LABEL = nls.localize('selectAndStartDebugging', "Select and Start Debugging"); - - constructor(id: string, label: string, - @IDebugService debugService: IDebugService, - @IKeybindingService keybindingService: IKeybindingService, - @IQuickInputService private readonly quickInputService: IQuickInputService - ) { - super(id, label, '', debugService, keybindingService); - } - - async run(): Promise { - this.quickInputService.quickAccess.show('debug '); - } -} - -export class RemoveBreakpointAction extends Action { - static readonly ID = 'workbench.debug.viewlet.action.removeBreakpoint'; - static readonly LABEL = nls.localize('removeBreakpoint', "Remove Breakpoint"); - - constructor(id: string, label: string, @IDebugService private readonly debugService: IDebugService) { - super(id, label, 'debug-action remove'); - } - - run(breakpoint: IBreakpoint): Promise { - return breakpoint instanceof Breakpoint ? this.debugService.removeBreakpoints(breakpoint.getId()) - : breakpoint instanceof FunctionBreakpoint ? this.debugService.removeFunctionBreakpoints(breakpoint.getId()) : this.debugService.removeDataBreakpoints(breakpoint.getId()); - } -} - -export class RemoveAllBreakpointsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.removeAllBreakpoints'; - static readonly LABEL = nls.localize('removeAllBreakpoints', "Remove All Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.breakpointsRemoveAll), debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - run(): Promise { - return Promise.all([this.debugService.removeBreakpoints(), this.debugService.removeFunctionBreakpoints(), this.debugService.removeDataBreakpoints()]); - } - - protected isEnabled(_: State): boolean { - const model = this.debugService.getModel(); - return (model.getBreakpoints().length > 0 || model.getFunctionBreakpoints().length > 0 || model.getDataBreakpoints().length > 0); - } -} - -export class EnableAllBreakpointsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.enableAllBreakpoints'; - static readonly LABEL = nls.localize('enableAllBreakpoints', "Enable All Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action enable-all-breakpoints', debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - run(): Promise { - return this.debugService.enableOrDisableBreakpoints(true); - } - - protected isEnabled(_: State): boolean { - const model = this.debugService.getModel(); - return (>model.getBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getExceptionBreakpoints()).some(bp => !bp.enabled); - } -} - -export class DisableAllBreakpointsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.disableAllBreakpoints'; - static readonly LABEL = nls.localize('disableAllBreakpoints', "Disable All Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action disable-all-breakpoints', debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - run(): Promise { - return this.debugService.enableOrDisableBreakpoints(false); - } - - protected isEnabled(_: State): boolean { - const model = this.debugService.getModel(); - return (>model.getBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getExceptionBreakpoints()).some(bp => bp.enabled); - } -} - -export class ToggleBreakpointsActivatedAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.toggleBreakpointsActivatedAction'; - static readonly ACTIVATE_LABEL = nls.localize('activateBreakpoints', "Activate Breakpoints"); - static readonly DEACTIVATE_LABEL = nls.localize('deactivateBreakpoints', "Deactivate Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.breakpointsActivate), debugService, keybindingService); - this.updateLabel(this.debugService.getModel().areBreakpointsActivated() ? ToggleBreakpointsActivatedAction.DEACTIVATE_LABEL : ToggleBreakpointsActivatedAction.ACTIVATE_LABEL); - - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => { - this.updateLabel(this.debugService.getModel().areBreakpointsActivated() ? ToggleBreakpointsActivatedAction.DEACTIVATE_LABEL : ToggleBreakpointsActivatedAction.ACTIVATE_LABEL); - this.updateEnablement(); - })); - } - - run(): Promise { - return this.debugService.setBreakpointsActivated(!this.debugService.getModel().areBreakpointsActivated()); - } - - protected isEnabled(_: State): boolean { - return !!(this.debugService.getModel().getFunctionBreakpoints().length || this.debugService.getModel().getBreakpoints().length || this.debugService.getModel().getDataBreakpoints().length); - } -} - -export class ReapplyBreakpointsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.reapplyBreakpointsAction'; - static readonly LABEL = nls.localize('reapplyAllBreakpoints', "Reapply All Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, '', debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - run(): Promise { - return this.debugService.setBreakpointsActivated(true); - } - - protected isEnabled(state: State): boolean { - const model = this.debugService.getModel(); - return (state === State.Running || state === State.Stopped) && - ((model.getFunctionBreakpoints().length + model.getBreakpoints().length + model.getExceptionBreakpoints().length + model.getDataBreakpoints().length) > 0); - } -} - -export class AddFunctionBreakpointAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.addFunctionBreakpointAction'; - static readonly LABEL = nls.localize('addFunctionBreakpoint', "Add Function Breakpoint"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.watchExpressionsAddFuncBreakpoint), debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - async run(): Promise { - this.debugService.addFunctionBreakpoint(); - } - - protected isEnabled(_: State): boolean { - return !this.debugService.getViewModel().getSelectedBreakpoint() - && this.debugService.getModel().getFunctionBreakpoints().every(fbp => !!fbp.name); - } -} - -export class AddWatchExpressionAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.addWatchExpression'; - static readonly LABEL = nls.localize('addWatchExpression', "Add Expression"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.watchExpressionsAdd), debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeWatchExpressions(() => this.updateEnablement())); - this._register(this.debugService.getViewModel().onDidSelectExpression(() => this.updateEnablement())); - } - - async run(): Promise { - this.debugService.addWatchExpression(); - } - - protected isEnabled(_: State): boolean { - const focusedExpression = this.debugService.getViewModel().getSelectedExpression(); - return this.debugService.getModel().getWatchExpressions().every(we => !!we.name && we !== focusedExpression); - } -} - -export class RemoveAllWatchExpressionsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.removeAllWatchExpressions'; - static readonly LABEL = nls.localize('removeAllWatchExpressions', "Remove All Expressions"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.watchExpressionsRemoveAll), debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeWatchExpressions(() => this.updateEnablement())); - } - - async run(): Promise { - this.debugService.removeWatchExpressions(); - } - - protected isEnabled(_: State): boolean { - return this.debugService.getModel().getWatchExpressions().length > 0; - } -} - -export class FocusSessionAction extends AbstractDebugAction { - static readonly ID = 'workbench.action.debug.focusProcess'; - static readonly LABEL = nls.localize('focusSession', "Focus Session"); - - constructor(id: string, label: string, - @IDebugService debugService: IDebugService, - @IKeybindingService keybindingService: IKeybindingService, - @IEditorService private readonly editorService: IEditorService - ) { - super(id, label, '', debugService, keybindingService); - } - - async run(session: IDebugSession): Promise { - await this.debugService.focusStackFrame(undefined, undefined, session, true); - const stackFrame = this.debugService.getViewModel().focusedStackFrame; - if (stackFrame) { - await stackFrame.openInEditor(this.editorService, true); - } - } -} - -export class CopyValueAction extends Action { - static readonly ID = 'workbench.debug.viewlet.action.copyValue'; - static readonly LABEL = nls.localize('copyValue', "Copy Value"); - - constructor( - id: string, label: string, private value: Variable | Expression, private context: string, - @IDebugService private readonly debugService: IDebugService, - @IClipboardService private readonly clipboardService: IClipboardService - ) { - super(id, label); - this._enabled = (this.value instanceof Expression) || (this.value instanceof Variable && !!this.value.evaluateName); - } - - async run(): Promise { - const stackFrame = this.debugService.getViewModel().focusedStackFrame; - const session = this.debugService.getViewModel().focusedSession; - if (!stackFrame || !session) { - return; - } - - const context = session.capabilities.supportsClipboardContext ? 'clipboard' : this.context; - const toEvaluate = this.value instanceof Variable ? (this.value.evaluateName || this.value.value) : this.value.name; - - try { - const evaluation = await session.evaluate(toEvaluate, stackFrame.frameId, context); - if (evaluation) { - this.clipboardService.writeText(evaluation.body.result); - } - } catch (e) { - this.clipboardService.writeText(typeof this.value === 'string' ? this.value : this.value.value); - } - } -} diff --git a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts index 249f1e69d..bf1fc62fe 100644 --- a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts @@ -54,11 +54,6 @@ export class AdapterManager implements IAdapterManager { if (!rawAdapter.type || (typeof rawAdapter.type !== 'string')) { added.collector.error(nls.localize('debugNoType', "Debugger 'type' can not be omitted and must be of type 'string'.")); } - if (rawAdapter.enableBreakpointsFor && rawAdapter.enableBreakpointsFor.languageIds) { - rawAdapter.enableBreakpointsFor.languageIds.forEach(modeId => { - this.breakpointModeIdsSet.add(modeId); - }); - } if (rawAdapter.type !== '*') { const existing = this.getDebugger(rawAdapter.type); diff --git a/src/vs/workbench/contrib/debug/browser/debugColors.ts b/src/vs/workbench/contrib/debug/browser/debugColors.ts index df54038d0..476f8571e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugColors.ts +++ b/src/vs/workbench/contrib/debug/browser/debugColors.ts @@ -4,8 +4,28 @@ *--------------------------------------------------------------------------------------------*/ import { registerColor, foreground, editorInfoForeground, editorWarningForeground, errorForeground, badgeBackground, badgeForeground, listDeemphasizedForeground, contrastBorder, inputBorder } from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; +import { localize } from 'vs/nls'; +import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; + +export const debugToolBarBackground = registerColor('debugToolBar.background', { + dark: '#333333', + light: '#F3F3F3', + hc: '#000000' +}, localize('debugToolBarBackground', "Debug toolbar background color.")); + +export const debugToolBarBorder = registerColor('debugToolBar.border', { + dark: null, + light: null, + hc: null +}, localize('debugToolBarBorder', "Debug toolbar border color.")); + +export const debugIconStartForeground = registerColor('debugIcon.startForeground', { + dark: '#89D185', + light: '#388A34', + hc: '#89D185' +}, localize('debugIcon.startForeground', "Debug toolbar icon for start debugging.")); export function registerColors() { @@ -28,6 +48,62 @@ export function registerColors() { const debugConsoleSourceForeground = registerColor('debugConsole.sourceForeground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for source filenames in debug REPL console.'); const debugConsoleInputIconForeground = registerColor('debugConsoleInputIcon.foreground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for debug console input marker icon.'); + + + const debugIconPauseForeground = registerColor('debugIcon.pauseForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.pauseForeground', "Debug toolbar icon for pause.")); + + const debugIconStopForeground = registerColor('debugIcon.stopForeground', { + dark: '#F48771', + light: '#A1260D', + hc: '#F48771' + }, localize('debugIcon.stopForeground', "Debug toolbar icon for stop.")); + + const debugIconDisconnectForeground = registerColor('debugIcon.disconnectForeground', { + dark: '#F48771', + light: '#A1260D', + hc: '#F48771' + }, localize('debugIcon.disconnectForeground', "Debug toolbar icon for disconnect.")); + + const debugIconRestartForeground = registerColor('debugIcon.restartForeground', { + dark: '#89D185', + light: '#388A34', + hc: '#89D185' + }, localize('debugIcon.restartForeground', "Debug toolbar icon for restart.")); + + const debugIconStepOverForeground = registerColor('debugIcon.stepOverForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.stepOverForeground', "Debug toolbar icon for step over.")); + + const debugIconStepIntoForeground = registerColor('debugIcon.stepIntoForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.stepIntoForeground', "Debug toolbar icon for step into.")); + + const debugIconStepOutForeground = registerColor('debugIcon.stepOutForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.stepOutForeground', "Debug toolbar icon for step over.")); + + const debugIconContinueForeground = registerColor('debugIcon.continueForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.continueForeground', "Debug toolbar icon for continue.")); + + const debugIconStepBackForeground = registerColor('debugIcon.stepBackForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.stepBackForeground', "Debug toolbar icon for step back.")); + registerThemingParticipant((theme, collector) => { // All these colours provide a default value so they will never be undefined, hence the `!` const badgeBackgroundColor = theme.getColor(badgeBackground)!; @@ -186,5 +262,55 @@ export function registerColors() { } `); } + + const debugIconStartColor = theme.getColor(debugIconStartForeground); + if (debugIconStartColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStart)} { color: ${debugIconStartColor} !important; }`); + } + + const debugIconPauseColor = theme.getColor(debugIconPauseForeground); + if (debugIconPauseColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugPause)} { color: ${debugIconPauseColor} !important; }`); + } + + const debugIconStopColor = theme.getColor(debugIconStopForeground); + if (debugIconStopColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStop)} { color: ${debugIconStopColor} !important; }`); + } + + const debugIconDisconnectColor = theme.getColor(debugIconDisconnectForeground); + if (debugIconDisconnectColor) { + collector.addRule(`.monaco-workbench .debug-view-content ${ThemeIcon.asCSSSelector(icons.debugDisconnect)}, .monaco-workbench .debug-toolbar ${ThemeIcon.asCSSSelector(icons.debugDisconnect)} { color: ${debugIconDisconnectColor} !important; }`); + } + + const debugIconRestartColor = theme.getColor(debugIconRestartForeground); + if (debugIconRestartColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugRestart)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugRestartFrame)} { color: ${debugIconRestartColor} !important; }`); + } + + const debugIconStepOverColor = theme.getColor(debugIconStepOverForeground); + if (debugIconStepOverColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepOver)} { color: ${debugIconStepOverColor} !important; }`); + } + + const debugIconStepIntoColor = theme.getColor(debugIconStepIntoForeground); + if (debugIconStepIntoColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepInto)} { color: ${debugIconStepIntoColor} !important; }`); + } + + const debugIconStepOutColor = theme.getColor(debugIconStepOutForeground); + if (debugIconStepOutColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepOut)} { color: ${debugIconStepOutColor} !important; }`); + } + + const debugIconContinueColor = theme.getColor(debugIconContinueForeground); + if (debugIconContinueColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugContinue)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugReverseContinue)} { color: ${debugIconContinueColor} !important; }`); + } + + const debugIconStepBackColor = theme.getColor(debugIconStepBackForeground); + if (debugIconStepBackColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepBack)} { color: ${debugIconStepBackColor} !important; }`); + } }); } diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 47aaedfb9..3b353cd61 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -9,7 +9,7 @@ import { List } from 'vs/base/browser/ui/list/listWidget'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IListService } from 'vs/platform/list/browser/listService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, CONTEXT_BREAKPOINT_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, IDebugConfiguration, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, IDebugConfiguration, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, REPL_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE, State, getStateLabel, CONTEXT_BREAKPOINT_INPUT_FOCUSED } from 'vs/workbench/contrib/debug/common/debug'; import { Expression, Variable, Breakpoint, FunctionBreakpoint, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -23,12 +23,13 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { PanelFocusContext } from 'vs/workbench/common/panel'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IViewsService } from 'vs/workbench/common/views'; +import { deepClone } from 'vs/base/common/objects'; export const ADD_CONFIGURATION_ID = 'debug.addConfiguration'; export const TOGGLE_INLINE_BREAKPOINT_ID = 'editor.debug.action.toggleInlineBreakpoint'; @@ -47,6 +48,13 @@ export const RESTART_FRAME_ID = 'workbench.action.debug.restartFrame'; export const CONTINUE_ID = 'workbench.action.debug.continue'; export const FOCUS_REPL_ID = 'workbench.debug.action.focusRepl'; export const JUMP_TO_CURSOR_ID = 'debug.jumpToCursor'; +export const FOCUS_SESSION_ID = 'workbench.action.debug.focusProcess'; +export const SELECT_AND_START_ID = 'workbench.action.debug.selectandstart'; +export const DEBUG_CONFIGURE_COMMAND_ID = 'workbench.action.debug.configure'; +export const DEBUG_START_COMMAND_ID = 'workbench.action.debug.start'; +export const DEBUG_RUN_COMMAND_ID = 'workbench.action.debug.run'; +export const EDIT_EXPRESSION_COMMAND_ID = 'debug.renameWatchExpression'; +export const REMOVE_EXPRESSION_COMMAND_ID = 'debug.removeWatchExpression'; export const RESTART_LABEL = nls.localize('restartDebug', "Restart"); export const STEP_OVER_LABEL = nls.localize('stepOverDebug', "Step Over"); @@ -56,6 +64,11 @@ export const PAUSE_LABEL = nls.localize('pauseDebug', "Pause"); export const DISCONNECT_LABEL = nls.localize('disconnect', "Disconnect"); export const STOP_LABEL = nls.localize('stop', "Stop"); export const CONTINUE_LABEL = nls.localize('continueDebug', "Continue"); +export const FOCUS_SESSION_LABEL = nls.localize('focusSession', "Focus Session"); +export const SELECT_AND_START_LABEL = nls.localize('selectAndStartDebugging', "Select and Start Debugging"); +export const DEBUG_CONFIGURE_LABEL = nls.localize('openLaunchJson', "Open {0}", 'launch.json'); +export const DEBUG_START_LABEL = nls.localize('startDebug', "Start Debugging"); +export const DEBUG_RUN_LABEL = nls.localize('startWithoutDebugging', "Start Without Debugging"); interface CallStackContext { sessionId: string; @@ -322,7 +335,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CONTINUE_ID, - weight: KeybindingWeight.WorkbenchContrib, + weight: KeybindingWeight.WorkbenchContrib + 10, // Use a stronger weight to get priority over start debugging F5 shortcut primary: KeyCode.F5, when: CONTEXT_IN_DEBUG_MODE, handler: (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { @@ -346,6 +359,53 @@ export function registerCommands(): void { } }); + CommandsRegistry.registerCommand({ + id: FOCUS_SESSION_ID, + handler: async (accessor: ServicesAccessor, session: IDebugSession) => { + const debugService = accessor.get(IDebugService); + const editorService = accessor.get(IEditorService); + await debugService.focusStackFrame(undefined, undefined, session, true); + const stackFrame = debugService.getViewModel().focusedStackFrame; + if (stackFrame) { + await stackFrame.openInEditor(editorService, true); + } + } + }); + + CommandsRegistry.registerCommand({ + id: SELECT_AND_START_ID, + handler: async (accessor: ServicesAccessor) => { + const quickInputService = accessor.get(IQuickInputService); + quickInputService.quickAccess.show('debug '); + } + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: DEBUG_START_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.F5, + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing))), + handler: async (accessor: ServicesAccessor, debugStartOptions?: { noDebug: boolean }) => { + const debugService = accessor.get(IDebugService); + let { launch, name, getConfig } = debugService.getConfigurationManager().selectedConfiguration; + const config = await getConfig(); + const clonedConfig = deepClone(config); + await debugService.startDebugging(launch, clonedConfig || name, { noDebug: debugStartOptions && debugStartOptions.noDebug }); + } + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: DEBUG_RUN_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.F5, + mac: { primary: KeyMod.WinCtrl | KeyCode.F5 }, + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing))), + handler: async (accessor: ServicesAccessor) => { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand(DEBUG_START_COMMAND_ID, { noDebug: true }); + } + }); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.toggleBreakpoint', weight: KeybindingWeight.WorkbenchContrib + 5, @@ -389,22 +449,27 @@ export function registerCommands(): void { }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'debug.renameWatchExpression', + id: EDIT_EXPRESSION_COMMAND_ID, weight: KeybindingWeight.WorkbenchContrib + 5, when: CONTEXT_WATCH_EXPRESSIONS_FOCUSED, primary: KeyCode.F2, mac: { primary: KeyCode.Enter }, - handler: (accessor) => { - const listService = accessor.get(IListService); + handler: (accessor: ServicesAccessor, expression: Expression | unknown) => { const debugService = accessor.get(IDebugService); - const focused = listService.lastFocusedList; - - if (focused) { - const elements = focused.getFocus(); - if (Array.isArray(elements) && elements[0] instanceof Expression) { - debugService.getViewModel().setSelectedExpression(elements[0]); + if (!(expression instanceof Expression)) { + const listService = accessor.get(IListService); + const focused = listService.lastFocusedList; + if (focused) { + const elements = focused.getFocus(); + if (Array.isArray(elements) && elements[0] instanceof Expression) { + expression = elements[0]; + } } } + + if (expression instanceof Expression) { + debugService.getViewModel().setSelectedExpression(expression); + } } }); @@ -429,16 +494,21 @@ export function registerCommands(): void { }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'debug.removeWatchExpression', + id: REMOVE_EXPRESSION_COMMAND_ID, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_EXPRESSION_SELECTED.toNegated()), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace }, - handler: (accessor) => { - const listService = accessor.get(IListService); + handler: (accessor: ServicesAccessor, expression: Expression | unknown) => { const debugService = accessor.get(IDebugService); - const focused = listService.lastFocusedList; + if (expression instanceof Expression) { + debugService.removeWatchExpressions(expression.getId()); + return; + } + + const listService = accessor.get(IListService); + const focused = listService.lastFocusedList; if (focused) { let elements = focused.getFocus(); if (Array.isArray(elements) && elements[0] instanceof Expression) { @@ -455,7 +525,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.removeBreakpoint', weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_SELECTED.toNegated()), + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_INPUT_FOCUSED.toNegated()), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace }, handler: (accessor) => { diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index d8ba1d554..bb92347d7 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { ServicesAccessor, registerEditorAction, EditorAction, IActionOptions } from 'vs/editor/browser/editorExtensions'; +import { registerEditorAction, EditorAction, IActionOptions, EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, WATCH_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_EXCEPTION_WIDGET_VISIBLE } from 'vs/workbench/contrib/debug/common/debug'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -24,24 +24,35 @@ import { Position } from 'vs/editor/common/core/position'; import { URI } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import { raceTimeout } from 'vs/base/common/async'; +import { registerAction2, MenuId } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export const TOGGLE_BREAKPOINT_ID = 'editor.debug.action.toggleBreakpoint'; -class ToggleBreakpointAction extends EditorAction { +class ToggleBreakpointAction extends EditorAction2 { constructor() { super({ - id: TOGGLE_BREAKPOINT_ID, - label: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"), - alias: 'Debug: Toggle Breakpoint', + id: 'editor.debug.action.toggleBreakpoint', + title: { + value: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"), + original: 'Toggle Breakpoint', + mnemonicTitle: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") + }, + f1: true, precondition: CONTEXT_DEBUGGERS_AVAILABLE, - kbOpts: { - kbExpr: EditorContextKeys.editorTextFocus, + keybinding: { + when: EditorContextKeys.editorTextFocus, primary: KeyCode.F9, weight: KeybindingWeight.EditorContrib + }, + menu: { + when: CONTEXT_DEBUGGERS_AVAILABLE, + id: MenuId.MenubarDebugMenu, + group: '4_new_breakpoint', + order: 1 } }); } - async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]): Promise { if (editor.hasModel()) { const debugService = accessor.get(IDebugService); const modelUri = editor.getModel().uri; @@ -49,33 +60,39 @@ class ToggleBreakpointAction extends EditorAction { // Does not account for multi line selections, Set to remove multiple cursor on the same line const lineNumbers = [...new Set(editor.getSelections().map(s => s.getPosition().lineNumber))]; - return Promise.all(lineNumbers.map(line => { + await Promise.all(lineNumbers.map(async line => { const bps = debugService.getModel().getBreakpoints({ lineNumber: line, uri: modelUri }); if (bps.length) { - return Promise.all(bps.map(bp => debugService.removeBreakpoints(bp.getId()))); + await Promise.all(bps.map(bp => debugService.removeBreakpoints(bp.getId()))); } else if (canSet) { - return (debugService.addBreakpoints(modelUri, [{ lineNumber: line }])); - } else { - return []; + await debugService.addBreakpoints(modelUri, [{ lineNumber: line }]); } })); } } } -export const TOGGLE_CONDITIONAL_BREAKPOINT_ID = 'editor.debug.action.conditionalBreakpoint'; -class ConditionalBreakpointAction extends EditorAction { - +class ConditionalBreakpointAction extends EditorAction2 { constructor() { super({ - id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, - label: nls.localize('conditionalBreakpointEditorAction', "Debug: Add Conditional Breakpoint..."), - alias: 'Debug: Add Conditional Breakpoint...', - precondition: CONTEXT_DEBUGGERS_AVAILABLE + id: 'editor.debug.action.conditionalBreakpoint', + title: { + value: nls.localize('conditionalBreakpointEditorAction', "Debug: Add Conditional Breakpoint..."), + original: 'Debug: Add Conditional Breakpoint...', + mnemonicTitle: nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Conditional Breakpoint...") + }, + f1: true, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, + menu: { + id: MenuId.MenubarNewBreakpointMenu, + group: '1_breakpoints', + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE + } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]): void { const debugService = accessor.get(IDebugService); const position = editor.getPosition(); @@ -85,19 +102,28 @@ class ConditionalBreakpointAction extends EditorAction { } } -export const ADD_LOG_POINT_ID = 'editor.debug.action.addLogPoint'; -class LogPointAction extends EditorAction { +class LogPointAction extends EditorAction2 { constructor() { super({ - id: ADD_LOG_POINT_ID, - label: nls.localize('logPointEditorAction', "Debug: Add Logpoint..."), - alias: 'Debug: Add Logpoint...', - precondition: CONTEXT_DEBUGGERS_AVAILABLE + id: 'editor.debug.action.addLogPoint', + title: { + value: nls.localize('logPointEditorAction', "Debug: Add Logpoint..."), + original: 'Debug: Add Logpoint...', + mnemonicTitle: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Logpoint...") + }, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, + f1: true, + menu: { + id: MenuId.MenubarNewBreakpointMenu, + group: '1_breakpoints', + order: 4, + when: CONTEXT_DEBUGGERS_AVAILABLE + } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]): void { const debugService = accessor.get(IDebugService); const position = editor.getPosition(); @@ -476,9 +502,9 @@ class CloseExceptionWidgetAction extends EditorAction { } export function registerEditorActions(): void { - registerEditorAction(ToggleBreakpointAction); - registerEditorAction(ConditionalBreakpointAction); - registerEditorAction(LogPointAction); + registerAction2(ToggleBreakpointAction); + registerAction2(ConditionalBreakpointAction); + registerAction2(LogPointAction); registerEditorAction(RunToCursorAction); registerEditorAction(StepIntoTargetsAction); registerEditorAction(SelectionToReplAction); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index dbd53aa5b..f3fa36a2c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -23,7 +23,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ICommandService } from 'vs/platform/commands/common/commands'; import { IDebugEditorContribution, IDebugService, State, IStackFrame, IDebugConfiguration, IExpression, IExceptionInfo, IDebugSession, CONTEXT_EXCEPTION_WIDGET_VISIBLE } from 'vs/workbench/contrib/debug/common/debug'; import { ExceptionWidget } from 'vs/workbench/contrib/debug/browser/exceptionWidget'; -import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; +import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { Position } from 'vs/editor/common/core/position'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { memoize, createMemoizer } from 'vs/base/common/decorators'; @@ -339,7 +339,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { if (this.hoverRange) { this.showHover(this.hoverRange, false); } - }, hoverOption.delay); + }, hoverOption.delay * 2); this.toDispose.push(scheduler); return scheduler; @@ -432,7 +432,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { this.closeExceptionWidget(); } else if (sameUri) { const exceptionInfo = await focusedSf.thread.exceptionInfo; - if (exceptionInfo && exceptionSf.range.startLineNumber && exceptionSf.range.startColumn) { + if (exceptionInfo) { this.showExceptionWidget(exceptionInfo, this.debugService.getViewModel().focusedSession, exceptionSf.range.startLineNumber, exceptionSf.range.startColumn); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index d828f5bf4..3bbddfa95 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -105,7 +105,7 @@ export class DebugHoverWidget implements IContentWidget { this.treeContainer = dom.append(this.complexValueContainer, $('.debug-hover-tree')); this.treeContainer.setAttribute('role', 'tree'); const tip = dom.append(this.complexValueContainer, $('.tip')); - tip.textContent = nls.localize('quickTip', 'Hold {0} key to switch to editor language hover', isMacintosh ? 'Option' : 'Alt'); + tip.textContent = nls.localize({ key: 'quickTip', comment: ['"switch to editor language hover" means to show the programming language hover widget instead of the debug hover'] }, 'Hold {0} key to switch to editor language hover', isMacintosh ? 'Option' : 'Alt'); const dataSource = new DebugHoverDataSource(); this.tree = >this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'DebugHover', this.treeContainer, new DebugHoverDelegate(), [this.instantiationService.createInstance(VariablesRenderer)], diff --git a/src/vs/workbench/contrib/debug/browser/debugIcons.ts b/src/vs/workbench/contrib/debug/browser/debugIcons.ts index b2203e381..b031cd582 100644 --- a/src/vs/workbench/contrib/debug/browser/debugIcons.ts +++ b/src/vs/workbench/contrib/debug/browser/debugIcons.ts @@ -15,25 +15,37 @@ export const callStackViewIcon = registerIcon('callstack-view-icon', Codicon.deb export const breakpointsViewIcon = registerIcon('breakpoints-view-icon', Codicon.debugAlt, localize('breakpointsViewIcon', 'View icon of the breakpoints view.')); export const loadedScriptsViewIcon = registerIcon('loaded-scripts-view-icon', Codicon.debugAlt, localize('loadedScriptsViewIcon', 'View icon of the loaded scripts view.')); -export const debugBreakpoint = registerIcon('debug-breakpoint', Codicon.debugBreakpoint, localize('debugBreakpoint', 'Icon for breakpoints.')); -export const debugBreakpointDisabled = registerIcon('debug-breakpoint-disabled', Codicon.debugBreakpointDisabled, localize('debugBreakpointDisabled', 'Icon for disabled breakpoints.')); -export const debugBreakpointUnverified = registerIcon('debug-breakpoint-unverified', Codicon.debugBreakpointUnverified, localize('debugBreakpointUnverified', 'Icon for unverified breakpoints.')); -export const debugBreakpointHint = registerIcon('debug-hint', Codicon.debugHint, localize('debugBreakpointHint', 'Icon for breakpoint hints shown on hover in editor glyph margin.')); -export const debugBreakpointFunction = registerIcon('debug-breakpoint-function', Codicon.debugBreakpointFunction, localize('debugBreakpointFunction', 'Icon for function breakpoints.')); -export const debugBreakpointFunctionUnverified = registerIcon('debug-breakpoint-function-unverified', Codicon.debugBreakpointFunctionUnverified, localize('debugBreakpointFunctionUnverified', 'Icon for unverified function breakpoints.')); -export const debugBreakpointFunctionDisabled = registerIcon('debug-breakpoint-function-disabled', Codicon.debugBreakpointFunctionDisabled, localize('debugBreakpointFunctionDisabled', 'Icon for disabled function breakpoints.')); +export const breakpoint = { + regular: registerIcon('debug-breakpoint', Codicon.debugBreakpoint, localize('debugBreakpoint', 'Icon for breakpoints.')), + disabled: registerIcon('debug-breakpoint-disabled', Codicon.debugBreakpointDisabled, localize('debugBreakpointDisabled', 'Icon for disabled breakpoints.')), + unverified: registerIcon('debug-breakpoint-unverified', Codicon.debugBreakpointUnverified, localize('debugBreakpointUnverified', 'Icon for unverified breakpoints.')) +}; +export const functionBreakpoint = { + regular: registerIcon('debug-breakpoint-function', Codicon.debugBreakpointFunction, localize('debugBreakpointFunction', 'Icon for function breakpoints.')), + disabled: registerIcon('debug-breakpoint-function-disabled', Codicon.debugBreakpointFunctionDisabled, localize('debugBreakpointFunctionDisabled', 'Icon for disabled function breakpoints.')), + unverified: registerIcon('debug-breakpoint-function-unverified', Codicon.debugBreakpointFunctionUnverified, localize('debugBreakpointFunctionUnverified', 'Icon for unverified function breakpoints.')) +}; +export const conditionalBreakpoint = { + regular: registerIcon('debug-breakpoint-conditional', Codicon.debugBreakpointConditional, localize('debugBreakpointConditional', 'Icon for conditional breakpoints.')), + disabled: registerIcon('debug-breakpoint-conditional-disabled', Codicon.debugBreakpointConditionalDisabled, localize('debugBreakpointConditionalDisabled', 'Icon for disabled conditional breakpoints.')), + unverified: registerIcon('debug-breakpoint-conditional-unverified', Codicon.debugBreakpointConditionalUnverified, localize('debugBreakpointConditionalUnverified', 'Icon for unverified conditional breakpoints.')) +}; +export const dataBreakpoint = { + regular: registerIcon('debug-breakpoint-data', Codicon.debugBreakpointData, localize('debugBreakpointData', 'Icon for data breakpoints.')), + disabled: registerIcon('debug-breakpoint-data-disabled', Codicon.debugBreakpointDataDisabled, localize('debugBreakpointDataDisabled', 'Icon for disabled data breakpoints.')), + unverified: registerIcon('debug-breakpoint-data-unverified', Codicon.debugBreakpointDataUnverified, localize('debugBreakpointDataUnverified', 'Icon for unverified data breakpoints.')), +}; +export const logBreakpoint = { + regular: registerIcon('debug-breakpoint-log', Codicon.debugBreakpointLog, localize('debugBreakpointLog', 'Icon for log breakpoints.')), + disabled: registerIcon('debug-breakpoint-log-disabled', Codicon.debugBreakpointLogDisabled, localize('debugBreakpointLogDisabled', 'Icon for disabled log breakpoint.')), + unverified: registerIcon('debug-breakpoint-log-unverified', Codicon.debugBreakpointLogUnverified, localize('debugBreakpointLogUnverified', 'Icon for unverified log breakpoints.')), +}; +export const debugBreakpointHint = registerIcon('debug-hint', Codicon.debugHint, localize('debugBreakpointHint', 'Icon for breakpoint hints shown on hover in editor glyph margin.')); export const debugBreakpointUnsupported = registerIcon('debug-breakpoint-unsupported', Codicon.debugBreakpointUnsupported, localize('debugBreakpointUnsupported', 'Icon for unsupported breakpoints.')); -export const debugBreakpointConditionalUnverified = registerIcon('debug-breakpoint-conditional-unverified', Codicon.debugBreakpointConditionalUnverified, localize('debugBreakpointConditionalUnverified', 'Icon for unverified conditional breakpoints.')); -export const debugBreakpointConditional = registerIcon('debug-breakpoint-conditional', Codicon.debugBreakpointConditional, localize('debugBreakpointConditional', 'Icon for conditional breakpoints.')); -export const debugBreakpointConditionalDisabled = registerIcon('debug-breakpoint-conditional-disabled', Codicon.debugBreakpointConditionalDisabled, localize('debugBreakpointConditionalDisabled', 'Icon for disabled conditional breakpoints.')); -export const debugBreakpointDataUnverified = registerIcon('debug-breakpoint-data-unverified', Codicon.debugBreakpointDataUnverified, localize('debugBreakpointDataUnverified', 'Icon for unverified data breakpoints.')); -export const debugBreakpointData = registerIcon('debug-breakpoint-data', Codicon.debugBreakpointData, localize('debugBreakpointData', 'Icon for data breakpoints.')); -export const debugBreakpointDataDisabled = registerIcon('debug-breakpoint-data-disabled', Codicon.debugBreakpointDataDisabled, localize('debugBreakpointDataDisabled', 'Icon for disabled data breakpoints.')); -export const debugBreakpointLogUnverified = registerIcon('debug-breakpoint-log-unverified', Codicon.debugBreakpointLogUnverified, localize('debugBreakpointLogUnverified', 'Icon for unverified log breakpoints.')); -export const debugBreakpointLog = registerIcon('debug-breakpoint-log', Codicon.debugBreakpointLog, localize('debugBreakpointLog', 'Icon for log breakpoints.')); -export const debugBreakpointLogDisabled = registerIcon('debug-breakpoint-log-disabled', Codicon.debugBreakpointLogDisabled, localize('debugBreakpointLogDisabled', 'Icon for disabled log breakpoint.')); +export const allBreakpoints = [breakpoint, functionBreakpoint, conditionalBreakpoint, dataBreakpoint, logBreakpoint]; + export const debugStackframe = registerIcon('debug-stackframe', Codicon.debugStackframe, localize('debugStackframe', 'Icon for a stackframe shown in the editor glyph margin.')); export const debugStackframeFocused = registerIcon('debug-stackframe-focused', Codicon.debugStackframeFocused, localize('debugStackframeFocused', 'Icon for a focused stackframe shown in the editor glyph margin.')); diff --git a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts index 2b6f530c4..3c920d136 100644 --- a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts +++ b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts @@ -115,7 +115,8 @@ export class StartDebugQuickAccessProvider extends PickerQuickAccessProvider { const pick = await provider.pick(); if (pick) { - await configManager.selectConfiguration(pick.launch, pick.config.name, pick.config, { type: pick.config.type }); + // Use the type of the provider, not of the config since config sometimes have subtypes (for example "node-terminal") + await configManager.selectConfiguration(pick.launch, pick.config.name, pick.config, { type: provider.type }); this.debugService.startDebugging(pick.launch, pick.config); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index e05a471f5..7631d5406 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -17,7 +17,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; import { DebugModel, FunctionBreakpoint, Breakpoint, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { ViewModel } from 'vs/workbench/contrib/debug/common/debugViewModel'; -import * as debugactions from 'vs/workbench/contrib/debug/browser/debugActions'; import { ConfigurationManager } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; import { VIEWLET_ID as EXPLORER_VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -25,7 +24,6 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { parse, getFirstFrame } from 'vs/base/common/console'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IAction, Action } from 'vs/base/common/actions'; @@ -48,9 +46,9 @@ import { DebugTelemetry } from 'vs/workbench/contrib/debug/common/debugTelemetry import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { AdapterManager } from 'vs/workbench/contrib/debug/browser/debugAdapterManager'; import { ITextModel } from 'vs/editor/common/model'; +import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; export class DebugService implements IDebugService { declare readonly _serviceBrand: undefined; @@ -96,8 +94,7 @@ export class DebugService implements IDebugService { @IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService, @IActivityService private readonly activityService: IActivityService, @ICommandService private readonly commandService: ICommandService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + @IQuickInputService private readonly quickInputService: IQuickInputService ) { this.toDispose = []; @@ -149,16 +146,6 @@ export class DebugService implements IDebugService { session.disconnect(); } })); - this.toDispose.push(this.extensionHostDebugService.onLogToSession(event => { - const session = this.model.getSession(event.sessionId, true); - if (session) { - // extension logged output -> show it in REPL - const sev = event.log.severity === 'warn' ? severity.Warning : event.log.severity === 'error' ? severity.Error : severity.Info; - const { args, stack } = parse(event.log); - const frame = !!stack ? getFirstFrame(stack) : undefined; - session.logToRepl(sev, args, frame); - } - })); this.toDispose.push(this.viewModel.onDidFocusStackFrame(() => { this.onStateChange(); @@ -290,6 +277,11 @@ export class DebugService implements IDebugService { await this.extensionService.activateByEvent('onDebug'); if (!options?.parentSession) { await this.editorService.saveAll(); + const activeEditor = this.editorService.activeEditorPane; + if (activeEditor) { + // Make sure to save the active editor in case it is in untitled file it wont be saved as part of saveAll #111850 + await this.editorService.save({ editor: activeEditor.input, groupId: activeEditor.group.id }); + } } await this.configurationService.reloadConfiguration(launch ? launch.workspace : undefined); await this.extensionService.whenInstalledExtensionsRegistered(); @@ -302,15 +294,6 @@ export class DebugService implements IDebugService { if (typeof configOrName === 'string' && launch) { config = launch.getConfiguration(configOrName); compound = launch.getCompound(configOrName); - - const sessions = this.model.getSessions(); - const alreadyRunningMessage = nls.localize('configurationAlreadyRunning', "There is already a debug configuration \"{0}\" running.", configOrName); - if (sessions.some(s => (s.configuration.name === configOrName && s.root === launch.workspace) && (!launch || !launch.workspace || !s.root || this.uriIdentityService.extUri.isEqual(s.root.uri, launch.workspace.uri)))) { - throw new Error(alreadyRunningMessage); - } - if (compound && compound.configurations && sessions.some(p => compound!.configurations.indexOf(p.configuration.name) !== -1)) { - throw new Error(alreadyRunningMessage); - } } else if (typeof configOrName !== 'string') { config = configOrName; } @@ -784,7 +767,7 @@ export class DebugService implements IDebugService { } private async showError(message: string, errorActions: ReadonlyArray = []): Promise { - const configureAction = this.instantiationService.createInstance(debugactions.ConfigureAction, debugactions.ConfigureAction.ID, debugactions.ConfigureAction.LABEL); + const configureAction = new Action(DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, undefined, true, () => this.commandService.executeCommand(DEBUG_CONFIGURE_COMMAND_ID)); const actions = [...errorActions, configureAction]; const { choice } = await this.dialogService.show(severity.Error, message, actions.map(a => a.label).concat(nls.localize('cancel', "Cancel")), { cancelId: actions.length }); if (choice < actions.length) { @@ -917,12 +900,11 @@ export class DebugService implements IDebugService { } addFunctionBreakpoint(name?: string, id?: string): void { - const newFunctionBreakpoint = this.model.addFunctionBreakpoint(name || '', id); - this.viewModel.setSelectedBreakpoint(newFunctionBreakpoint); + this.model.addFunctionBreakpoint(name || '', id); } - async renameFunctionBreakpoint(id: string, newFunctionName: string): Promise { - this.model.renameFunctionBreakpoint(id, newFunctionName); + async updateFunctionBreakpoint(id: string, update: { name?: string, hitCondition?: string, condition?: string }): Promise { + this.model.updateFunctionBreakpoint(id, update); this.debugStorage.storeBreakpoints(this.model); await this.sendFunctionBreakpoints(); } @@ -946,6 +928,11 @@ export class DebugService implements IDebugService { await this.sendDataBreakpoints(); } + setExceptionBreakpoints(data: DebugProtocol.ExceptionBreakpointsFilter[]): void { + this.model.setExceptionBreakpoints(data); + this.debugStorage.storeBreakpoints(this.model); + } + async setExceptionBreakpointCondition(exceptionBreakpoint: IExceptionBreakpoint, condition: string | undefined): Promise { this.model.setExceptionBreakpointCondition(exceptionBreakpoint, condition); this.debugStorage.storeBreakpoints(this.model); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 9b8c75afa..ec549fd04 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -64,7 +64,7 @@ export class DebugSession implements IDebugSession { private readonly _onDidChangeREPLElements = new Emitter(); - private name: string | undefined; + private _name: string | undefined; private readonly _onDidChangeName = new Emitter(); constructor( @@ -146,15 +146,18 @@ export class DebugSession implements IDebugSession { getLabel(): string { const includeRoot = this.workspaceContextService.getWorkspace().folders.length > 1; - const name = this.name || this.configuration.name; - return includeRoot && this.root ? `${name} (${resources.basenameOrAuthority(this.root.uri)})` : name; + return includeRoot && this.root ? `${this.name} (${resources.basenameOrAuthority(this.root.uri)})` : this.name; } setName(name: string): void { - this.name = name; + this._name = name; this._onDidChangeName.fire(name); } + get name(): string { + return this._name || this.configuration.name; + } + get state(): State { if (!this.initialized) { return State.Initializing; @@ -253,7 +256,7 @@ export class DebugSession implements IDebugSession { this.initialized = true; this._onDidChangeState.fire(); - this.model.setExceptionBreakpoints((this.raw && this.raw.capabilities.exceptionBreakpointFilters) || []); + this.debugService.setExceptionBreakpoints((this.raw && this.raw.capabilities.exceptionBreakpointFilters) || []); } catch (err) { this.initialized = true; this._onDidChangeState.fire(); @@ -1015,8 +1018,8 @@ export class DebugSession implements IDebugSession { this._onDidProgressEnd.fire(event); })); this.rawListeners.push(this.raw.onDidInvalidated(async event => { - if (!(event.body.areas && event.body.areas.length === 1 && event.body.areas[0] === 'variables')) { - // If invalidated event only requires to update variables, do that, otherwise refatch threads https://github.com/microsoft/vscode/issues/106745 + if (!(event.body.areas && event.body.areas.length === 1 && (event.body.areas[0] === 'variables' || event.body.areas[0] === 'watch'))) { + // If invalidated event only requires to update variables or watch, do that, otherwise refatch threads https://github.com/microsoft/vscode/issues/106745 this.cancelAllRequests(); this.model.clearThreads(this.getId(), true); await this.fetchThreads(this.stoppedDetails); diff --git a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts index 689328eb3..cac07a988 100644 --- a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts +++ b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts @@ -169,12 +169,18 @@ export class DebugTaskRunner { return taskPromise.then(withUndefinedAsNull); }); - return new Promise((c, e) => { + return new Promise(async (c, e) => { + const waitForInput = new Promise(resolve => once(e => (e.kind === TaskEventKind.AcquiredInput) && e.taskId === task._id, this.taskService.onDidStateChange)(() => { + resolve(); + })); + promise.then(result => { taskStarted = true; c(result); }, error => e(error)); + await waitForInput; + setTimeout(() => { if (!taskStarted) { const errorMessage = typeof taskId === 'string' diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 8f62ac8d9..cfb91f7a3 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -8,28 +8,30 @@ import * as errors from 'vs/base/common/errors'; import * as browser from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; import * as arrays from 'vs/base/common/arrays'; +import { localize } from 'vs/nls'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, Separator } from 'vs/base/common/actions'; +import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IDebugConfiguration, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugConfiguration, IDebugService, State, CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; import { FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; -import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { registerThemingParticipant, IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { registerColor, contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; -import { localize } from 'vs/nls'; +import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { createAndFillInActionBarActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenu, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { FocusSessionAction } from 'vs/workbench/contrib/debug/browser/debugActions'; +import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, IMenuService, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { IContextKeyService, ContextKeyExpression, ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { debugToolBarBackground, debugToolBarBorder } from 'vs/workbench/contrib/debug/browser/debugColors'; +import { URI } from 'vs/base/common/uri'; +import { CONTINUE_LABEL, CONTINUE_ID, PAUSE_ID, STOP_ID, DISCONNECT_ID, STEP_OVER_ID, STEP_INTO_ID, RESTART_SESSION_ID, STEP_OUT_ID, STEP_BACK_ID, REVERSE_CONTINUE_ID, RESTART_LABEL, STEP_OUT_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, DISCONNECT_LABEL, STOP_LABEL, PAUSE_LABEL, FOCUS_SESSION_ID, FOCUS_SESSION_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; const DEBUG_TOOLBAR_POSITION_KEY = 'debug.actionswidgetposition'; const DEBUG_TOOLBAR_Y_KEY = 'debug.actionswidgety'; @@ -75,15 +77,10 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { this.actionBar = this._register(new ActionBar(actionBarContainer, { orientation: ActionsOrientation.HORIZONTAL, actionViewItemProvider: (action: IAction) => { - if (action.id === FocusSessionAction.ID) { + if (action.id === FOCUS_SESSION_ID) { return this.instantiationService.createInstance(FocusSessionActionViewItem, action, undefined); - } else if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); } - - return undefined; + return createActionViewItem(this.instantiationService, action); } })); @@ -94,7 +91,8 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { return this.hide(); } - const { actions, disposable } = DebugToolBar.getActions(this.debugToolBarMenu, this.debugService, this.instantiationService); + const actions: IAction[] = []; + const disposable = createAndFillInActionBarActions(this.debugToolBarMenu, { shouldForwardArgs: true }, actions, () => false); if (!arrays.equals(actions, this.activeActions, (first, second) => first.id === second.id && first.enabled === second.enabled)) { this.actionBar.clear(); this.actionBar.push(actions, { icon: true, label: false }); @@ -115,9 +113,11 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { private registerListeners(): void { this._register(this.debugService.onDidChangeState(() => this.updateScheduler.schedule())); - this._register(this.debugService.getViewModel().onDidFocusSession(() => this.updateScheduler.schedule())); - this._register(this.debugService.onDidNewSession(() => this.updateScheduler.schedule())); - this._register(this.configurationService.onDidChangeConfiguration(e => this.onDidConfigurationChange(e))); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('debug.toolBarLocation')) { + this.updateScheduler.schedule(); + } + })); this._register(this.debugToolBarMenu.onDidChange(() => this.updateScheduler.schedule())); this._register(this.actionBar.actionRunner.onDidRun((e: IRunEvent) => { // check for error @@ -225,12 +225,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { } } - private onDidConfigurationChange(event: IConfigurationChangeEvent): void { - if (event.affectsConfiguration('debug.hideActionBar') || event.affectsConfiguration('debug.toolBarLocation')) { - this.updateScheduler.schedule(); - } - } - private show(): void { if (this.isVisible) { this.setCoordinates(); @@ -251,19 +245,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { dom.hide(this.$el); } - static getActions(menu: IMenu, debugService: IDebugService, instantiationService: IInstantiationService): { actions: IAction[], disposable: IDisposable } { - const actions: IAction[] = []; - const disposable = createAndFillInActionBarActions(menu, undefined, actions, () => false); - if (debugService.getViewModel().isMultiSessionView()) { - actions.push(instantiationService.createInstance(FocusSessionAction, FocusSessionAction.ID, FocusSessionAction.LABEL)); - } - - return { - actions: actions.filter(a => !(a instanceof Separator)), // do not render separators for now - disposable - }; - } - dispose(): void { super.dispose(); @@ -276,127 +257,43 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { } } -export const debugToolBarBackground = registerColor('debugToolBar.background', { - dark: '#333333', - light: '#F3F3F3', - hc: '#000000' -}, localize('debugToolBarBackground', "Debug toolbar background color.")); +// Debug toolbar -export const debugToolBarBorder = registerColor('debugToolBar.border', { - dark: null, - light: null, - hc: null -}, localize('debugToolBarBorder', "Debug toolbar border color.")); +const registerDebugToolBarItem = (id: string, title: string, order: number, icon?: { light?: URI, dark?: URI } | ThemeIcon, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { + MenuRegistry.appendMenuItem(MenuId.DebugToolBar, { + group: 'navigation', + when, + order, + command: { + id, + title, + icon, + precondition + } + }); -export const debugIconStartForeground = registerColor('debugIcon.startForeground', { - dark: '#89D185', - light: '#388A34', - hc: '#89D185' -}, localize('debugIcon.startForeground', "Debug toolbar icon for start debugging.")); + // Register actions in debug viewlet when toolbar is docked + MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + group: 'navigation', + when: ContextKeyExpr.and(when, ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_STATE.notEqualsTo('inactive'), ContextKeyExpr.equals('config.debug.toolBarLocation', 'docked')), + order, + command: { + id, + title, + icon, + precondition + } + }); +}; -export const debugIconPauseForeground = registerColor('debugIcon.pauseForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.pauseForeground', "Debug toolbar icon for pause.")); - -export const debugIconStopForeground = registerColor('debugIcon.stopForeground', { - dark: '#F48771', - light: '#A1260D', - hc: '#F48771' -}, localize('debugIcon.stopForeground', "Debug toolbar icon for stop.")); - -export const debugIconDisconnectForeground = registerColor('debugIcon.disconnectForeground', { - dark: '#F48771', - light: '#A1260D', - hc: '#F48771' -}, localize('debugIcon.disconnectForeground', "Debug toolbar icon for disconnect.")); - -export const debugIconRestartForeground = registerColor('debugIcon.restartForeground', { - dark: '#89D185', - light: '#388A34', - hc: '#89D185' -}, localize('debugIcon.restartForeground', "Debug toolbar icon for restart.")); - -export const debugIconStepOverForeground = registerColor('debugIcon.stepOverForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.stepOverForeground', "Debug toolbar icon for step over.")); - -export const debugIconStepIntoForeground = registerColor('debugIcon.stepIntoForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.stepIntoForeground', "Debug toolbar icon for step into.")); - -export const debugIconStepOutForeground = registerColor('debugIcon.stepOutForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.stepOutForeground', "Debug toolbar icon for step over.")); - -export const debugIconContinueForeground = registerColor('debugIcon.continueForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.continueForeground', "Debug toolbar icon for continue.")); - -export const debugIconStepBackForeground = registerColor('debugIcon.stepBackForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.stepBackForeground', "Debug toolbar icon for step back.")); - -registerThemingParticipant((theme, collector) => { - - const debugIconStartColor = theme.getColor(debugIconStartForeground); - if (debugIconStartColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStart)} { color: ${debugIconStartColor} !important; }`); - } - - const debugIconPauseColor = theme.getColor(debugIconPauseForeground); - if (debugIconPauseColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugPause)} { color: ${debugIconPauseColor} !important; }`); - } - - const debugIconStopColor = theme.getColor(debugIconStopForeground); - if (debugIconStopColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStop)} { color: ${debugIconStopColor} !important; }`); - } - - const debugIconDisconnectColor = theme.getColor(debugIconDisconnectForeground); - if (debugIconDisconnectColor) { - collector.addRule(`.monaco-workbench .debug-view-content ${ThemeIcon.asCSSSelector(icons.debugDisconnect)}, .monaco-workbench .debug-toolbar ${ThemeIcon.asCSSSelector(icons.debugDisconnect)} { color: ${debugIconDisconnectColor} !important; }`); - } - - const debugIconRestartColor = theme.getColor(debugIconRestartForeground); - if (debugIconRestartColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugRestart)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugRestartFrame)} { color: ${debugIconRestartColor} !important; }`); - } - - const debugIconStepOverColor = theme.getColor(debugIconStepOverForeground); - if (debugIconStepOverColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepOver)} { color: ${debugIconStepOverColor} !important; }`); - } - - const debugIconStepIntoColor = theme.getColor(debugIconStepIntoForeground); - if (debugIconStepIntoColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepInto)} { color: ${debugIconStepIntoColor} !important; }`); - } - - const debugIconStepOutColor = theme.getColor(debugIconStepOutForeground); - if (debugIconStepOutColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepOut)} { color: ${debugIconStepOutColor} !important; }`); - } - - const debugIconContinueColor = theme.getColor(debugIconContinueForeground); - if (debugIconContinueColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugContinue)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugReverseContinue)} { color: ${debugIconContinueColor} !important; }`); - } - - const debugIconStepBackColor = theme.getColor(debugIconStepBackForeground); - if (debugIconStepBackColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepBack)} { color: ${debugIconStepBackColor} !important; }`); - } -}); +registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, 10, icons.debugContinue, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, icons.debugPause, CONTEXT_DEBUG_STATE.notEqualsTo('stopped'), CONTEXT_DEBUG_STATE.isEqualTo('running')); +registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, icons.debugStop, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); +registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, icons.debugDisconnect, CONTEXT_FOCUSED_SESSION_IS_ATTACH); +registerDebugToolBarItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, icons.debugStepOver, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, icons.debugStepInto, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, icons.debugStepOut, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(RESTART_SESSION_ID, RESTART_LABEL, 60, icons.debugRestart); +registerDebugToolBarItem(STEP_BACK_ID, localize('stepBackDebug', "Step Back"), 50, icons.debugStepBack, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(REVERSE_CONTINUE_ID, localize('reverseContinue', "Reverse"), 60, icons.debugReverseContinue, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(FOCUS_SESSION_ID, FOCUS_SESSION_LABEL, 100, undefined, CONTEXT_MULTI_SESSION_DEBUG); diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index 91187dd3a..d0749ded5 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -6,34 +6,36 @@ import 'vs/css!./media/debugViewlet'; import * as nls from 'vs/nls'; import { IAction, IActionViewItem } from 'vs/base/common/actions'; -import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, IDebugConfiguration, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; -import { StartAction, ConfigureAction, SelectAndStartAction, FocusSessionAction } from 'vs/workbench/contrib/debug/browser/debugActions'; +import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, REPL_VIEW_ID, CONTEXT_DEBUG_STATE, ILaunch, getStateLabel, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { StartDebugActionViewItem, FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IProgressService } from 'vs/platform/progress/common/progress'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { memoize } from 'vs/base/common/decorators'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; -import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { IMenu, MenuId, IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { ViewPaneContainer, ViewsSubMenu } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { MenuId, registerAction2, Action2, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { IContextKeyService, ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyDefinedExpr } from 'vs/platform/contextkey/common/contextkey'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; -import { ToggleViewAction } from 'vs/workbench/browser/actions/layoutActions'; -import { RunOnceScheduler } from 'vs/base/common/async'; import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { debugConsole } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { debugConfigure, debugConsole } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ToggleViewAction } from 'vs/workbench/browser/actions/layoutActions'; +import { FOCUS_SESSION_ID, SELECT_AND_START_ID, DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, DEBUG_START_LABEL, DEBUG_START_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; export class DebugViewPaneContainer extends ViewPaneContainer { @@ -41,9 +43,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer { private progressResolve: (() => void) | undefined; private breakpointView: ViewPane | undefined; private paneListeners = new Map(); - private debugToolBarMenu: IMenu | undefined; - private disposeOnTitleUpdate: IDisposable | undefined; - private updateToolBarScheduler: RunOnceScheduler; constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @@ -58,22 +57,13 @@ export class DebugViewPaneContainer extends ViewPaneContainer { @IExtensionService extensionService: IExtensionService, @IConfigurationService configurationService: IConfigurationService, @IContextViewService private readonly contextViewService: IContextViewService, - @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService ) { super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); - this.updateToolBarScheduler = this._register(new RunOnceScheduler(() => { - if (this.configurationService.getValue('debug').toolBarLocation === 'docked') { - this.updateTitleArea(); - } - }, 20)); - // When there are potential updates to the docked debug toolbar we need to update it this._register(this.debugService.onDidChangeState(state => this.onDebugServiceStateChange(state))); - this._register(this.debugService.onDidNewSession(() => this.updateToolBarScheduler.schedule())); - this._register(this.debugService.getViewModel().onDidFocusSession(() => this.updateToolBarScheduler.schedule())); this._register(this.contextKeyService.onDidChangeContext(e => { if (e.affectsSome(new Set([CONTEXT_DEBUG_UX_KEY]))) { @@ -104,83 +94,15 @@ export class DebugViewPaneContainer extends ViewPaneContainer { } } - @memoize - private get startAction(): StartAction { - return this._register(this.instantiationService.createInstance(StartAction, StartAction.ID, StartAction.LABEL)); - } - - @memoize - private get configureAction(): ConfigureAction { - return this._register(this.instantiationService.createInstance(ConfigureAction, ConfigureAction.ID, ConfigureAction.LABEL)); - } - - @memoize - private get toggleReplAction(): OpenDebugConsoleAction { - return this._register(this.instantiationService.createInstance(OpenDebugConsoleAction, OpenDebugConsoleAction.ID, OpenDebugConsoleAction.LABEL)); - } - - @memoize - private get selectAndStartAction(): SelectAndStartAction { - return this._register(this.instantiationService.createInstance(SelectAndStartAction, SelectAndStartAction.ID, nls.localize('startAdditionalSession', "Start Additional Session"))); - } - - getActions(): IAction[] { - if (CONTEXT_DEBUG_UX.getValue(this.contextKeyService) === 'simple') { - return []; - } - - if (!this.showInitialDebugActions) { - - if (!this.debugToolBarMenu) { - this.debugToolBarMenu = this.menuService.createMenu(MenuId.DebugToolBar, this.contextKeyService); - this._register(this.debugToolBarMenu); - this._register(this.debugToolBarMenu.onDidChange(() => this.updateToolBarScheduler.schedule())); - } - - const { actions, disposable } = DebugToolBar.getActions(this.debugToolBarMenu, this.debugService, this.instantiationService); - if (this.disposeOnTitleUpdate) { - dispose(this.disposeOnTitleUpdate); - } - this.disposeOnTitleUpdate = disposable; - - return actions; - } - - if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - return [this.toggleReplAction]; - } - - return [this.startAction, this.configureAction, this.toggleReplAction]; - } - - get showInitialDebugActions(): boolean { - const state = this.debugService.state; - return state === State.Inactive || this.configurationService.getValue('debug').toolBarLocation !== 'docked'; - } - - getSecondaryActions(): IAction[] { - if (this.showInitialDebugActions) { - return []; - } - - return [this.selectAndStartAction, this.configureAction, this.toggleReplAction]; - } - getActionViewItem(action: IAction): IActionViewItem | undefined { - if (action.id === StartAction.ID) { + if (action.id === DEBUG_START_COMMAND_ID) { this.startDebugActionViewItem = this.instantiationService.createInstance(StartDebugActionViewItem, null, action); return this.startDebugActionViewItem; } - if (action.id === FocusSessionAction.ID) { + if (action.id === FOCUS_SESSION_ID) { return new FocusSessionActionViewItem(action, undefined, this.debugService, this.themeService, this.contextViewService, this.configurationService); } - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - - return undefined; + return createActionViewItem(this.instantiationService, action); } focusView(id: string): void { @@ -201,8 +123,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer { return new Promise(resolve => this.progressResolve = resolve); }); } - - this.updateToolBarScheduler.schedule(); } addPanes(panes: { pane: ViewPane, size: number, index?: number }[]): void { @@ -236,22 +156,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer { } } -export class OpenDebugConsoleAction extends ToggleViewAction { - public static readonly ID = 'workbench.debug.action.toggleRepl'; - public static readonly LABEL = nls.localize('toggleDebugPanel', "Debug Console"); - - constructor( - id: string, - label: string, - @IViewsService viewsService: IViewsService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IContextKeyService contextKeyService: IContextKeyService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, REPL_VIEW_ID, viewsService, viewDescriptorService, contextKeyService, layoutService, ThemeIcon.asClassName(debugConsole)); - } -} - export class OpenDebugViewletAction extends ShowViewletAction { public static readonly ID = VIEWLET_ID; public static readonly LABEL = nls.localize('toggleDebugViewlet', "Show Run and Debug"); @@ -266,3 +170,139 @@ export class OpenDebugViewletAction extends ShowViewletAction { super(id, label, VIEWLET_ID, viewletService, editorGroupService, layoutService); } } + +MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_UX.notEqualsTo('simple'), WorkbenchStateContext.notEqualsTo('empty'), + ContextKeyExpr.or(CONTEXT_DEBUG_STATE.isEqualTo('inactive'), ContextKeyExpr.notEquals('config.debug.toolBarLocation', 'docked'))), + order: 10, + group: 'navigation', + command: { + precondition: CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing)), + id: DEBUG_START_COMMAND_ID, + title: DEBUG_START_LABEL + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: DEBUG_CONFIGURE_COMMAND_ID, + title: { + value: DEBUG_CONFIGURE_LABEL, + original: DEBUG_CONFIGURE_LABEL, + mnemonicTitle: nls.localize({ key: 'miOpenConfigurations', comment: ['&& denotes a mnemonic'] }, "Open &&Configurations") + }, + f1: true, + icon: debugConfigure, + precondition: CONTEXT_DEBUG_UX.notEqualsTo('simple'), + menu: [{ + id: MenuId.ViewContainerTitle, + group: 'navigation', + order: 20, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_UX.notEqualsTo('simple'), WorkbenchStateContext.notEqualsTo('empty'), + ContextKeyExpr.or(CONTEXT_DEBUG_STATE.isEqualTo('inactive'), ContextKeyExpr.notEquals('config.debug.toolBarLocation', 'docked'))) + }, { + id: MenuId.ViewContainerTitle, + order: 20, + // Show in debug viewlet secondary actions when debugging and debug toolbar is docked + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_STATE.notEqualsTo('inactive'), ContextKeyExpr.equals('config.debug.toolBarLocation', 'docked')) + }, { + id: MenuId.MenubarDebugMenu, + group: '2_configuration', + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const debugService = accessor.get(IDebugService); + const quickInputService = accessor.get(IQuickInputService); + const configurationManager = debugService.getConfigurationManager(); + let launch: ILaunch | undefined; + if (configurationManager.selectedConfiguration.name) { + launch = configurationManager.selectedConfiguration.launch; + } else { + const launches = configurationManager.getLaunches().filter(l => !l.hidden); + if (launches.length === 1) { + launch = launches[0]; + } else { + const picks = launches.map(l => ({ label: l.name, launch: l })); + const picked = await quickInputService.pick<{ label: string, launch: ILaunch }>(picks, { + activeItem: picks[0], + placeHolder: nls.localize({ key: 'selectWorkspaceFolder', comment: ['User picks a workspace folder or a workspace configuration file here. Workspace configuration files can contain settings and thus a launch.json configuration can be written into one.'] }, "Select a workspace folder to create a launch.json file in or add it to the workspace config file") + }); + if (picked) { + launch = picked.launch; + } + } + } + + if (launch) { + await launch.openConfigFile(false); + } + } +}); + +export const OPEN_REPL_COMMAND_ID = 'workbench.debug.action.toggleRepl'; +registerAction2(class extends Action2 { + constructor() { + super({ + id: OPEN_REPL_COMMAND_ID, + title: { + value: nls.localize('debugPanel', "Debug Console"), + original: 'Debug Console', + mnemonicTitle: nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console") + }, + f1: true, + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y, + weight: KeybindingWeight.WorkbenchContrib + }, + icon: debugConsole, + menu: [{ + id: MenuId.MenubarViewMenu, + group: '4_panels', + order: 2 + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + return accessor.get(IInstantiationService).createInstance(ToggleViewAction, OPEN_REPL_COMMAND_ID, 'Debug Console', REPL_VIEW_ID).run(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'debug.toggleReplIgnoreFocus', + title: nls.localize('debugPanel', "Debug Console"), + toggled: ContextKeyDefinedExpr.create(`view.${REPL_VIEW_ID}.visible`), + menu: [{ + id: ViewsSubMenu, + group: '3_toggleRepl', + order: 30, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID)) + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + if (viewsService.isViewVisible(REPL_VIEW_ID)) { + viewsService.closeView(REPL_VIEW_ID); + } else { + await viewsService.openView(REPL_VIEW_ID); + } + } +}); + +MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_STATE.notEqualsTo('inactive'), ContextKeyExpr.equals('config.debug.toolBarLocation', 'docked')), + order: 10, + command: { + id: SELECT_AND_START_ID, + title: nls.localize('startAdditionalSession', "Start Additional Session"), + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/linkDetector.ts b/src/vs/workbench/contrib/debug/browser/linkDetector.ts index e927b779b..14ea88967 100644 --- a/src/vs/workbench/contrib/debug/browser/linkDetector.ts +++ b/src/vs/workbench/contrib/debug/browser/linkDetector.ts @@ -3,16 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Schemas } from 'vs/base/common/network'; import * as osPath from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import * as nls from 'vs/nls'; import { IFileService } from 'vs/platform/files/common/files'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; const CONTROL_CODES = '\\u0000-\\u0020\\u007f-\\u009f'; const WEB_LINK_REGEX = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|data:|www\\.)[^\\s' + CONTROL_CODES + '"]{2,}[^\\s' + CONTROL_CODES + '"\')}\\],:;.!?]', 'ug'); @@ -97,8 +99,28 @@ export class LinkDetector { private createWebLink(url: string): Node { const link = this.createLink(url); - const uri = URI.parse(url); - this.decorateLink(link, () => this.openerService.open(uri, { allowTunneling: !!this.environmentService.remoteAuthority })); + + this.decorateLink(link, async () => { + const uri = URI.parse(url); + + if (uri.scheme === Schemas.file) { + // Just using fsPath here is unsafe: https://github.com/microsoft/vscode/issues/109076 + const fsPath = uri.fsPath; + const path = await this.pathService.path; + const fileUrl = osPath.normalize(((path.sep === osPath.posix.sep) && platform.isWindows) ? fsPath.replace(/\\/g, osPath.posix.sep) : fsPath); + + const resolvedLink = await this.fileService.resolve(URI.parse(fileUrl)); + if (!resolvedLink) { + return; + } + + await this.editorService.openEditor({ resource: resolvedLink.resource, options: { pinned: true } }); + return; + } + + this.openerService.open(url, { allowTunneling: !!this.environmentService.remoteAuthority }); + }); + return link; } @@ -108,14 +130,14 @@ export class LinkDetector { return document.createTextNode(text); } + const options = { selection: { startLineNumber: lineNumber, startColumn: columnNumber } }; if (path[0] === '.') { if (!workspaceFolder) { return document.createTextNode(text); } const uri = workspaceFolder.toResource(path); - const options = { selection: { startLineNumber: lineNumber, startColumn: columnNumber } }; const link = this.createLink(text); - this.decorateLink(link, () => this.editorService.openEditor({ resource: uri, options })); + this.decorateLink(link, (preserveFocus: boolean) => this.editorService.openEditor({ resource: uri, options: { ...options, preserveFocus } })); return link; } @@ -127,13 +149,13 @@ export class LinkDetector { } const link = this.createLink(text); + link.tabIndex = 0; const uri = URI.file(osPath.normalize(path)); this.fileService.resolve(uri).then(stat => { if (stat.isDirectory) { return; } - const options = { selection: { startLineNumber: lineNumber, startColumn: columnNumber } }; - this.decorateLink(link, () => this.editorService.openEditor({ resource: uri, options })); + this.decorateLink(link, (preserveFocus: boolean) => this.editorService.openEditor({ resource: uri, options: { ...options, preserveFocus } })); }).catch(() => { // If the uri can not be resolved we should not spam the console with error, remain quite #86587 }); @@ -146,9 +168,8 @@ export class LinkDetector { return link; } - private decorateLink(link: HTMLElement, onclick: () => void) { + private decorateLink(link: HTMLElement, onClick: (preserveFocus: boolean) => void) { link.classList.add('link'); - link.title = platform.isMacintosh ? nls.localize('fileLinkMac', "Cmd + click to follow link") : nls.localize('fileLink', "Ctrl + click to follow link"); link.onmousemove = (event) => { link.classList.toggle('pointer', platform.isMacintosh ? event.metaKey : event.ctrlKey); }; link.onmouseleave = () => link.classList.remove('pointer'); link.onclick = (event) => { @@ -156,12 +177,17 @@ export class LinkDetector { if (!selection || selection.type === 'Range') { return; // do not navigate when user is selecting } - if (!(platform.isMacintosh ? event.metaKey : event.ctrlKey)) { - return; - } event.preventDefault(); event.stopImmediatePropagation(); - onclick(); + onClick(false); + }; + link.onkeydown = e => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.Enter || event.keyCode === KeyCode.Space) { + event.preventDefault(); + event.stopPropagation(); + onClick(event.keyCode === KeyCode.Space); + } }; } diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index 198da9108..6ebf580c2 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { normalize, isAbsolute, posix } from 'vs/base/common/path'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 47cafc6ae..5487c7f7e 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -144,22 +144,22 @@ display: none; } -.debug-pane .debug-call-stack .monaco-list-row .monaco-action-bar { +.debug-pane .monaco-list-row .monaco-action-bar { display: none; flex-shrink: 0; margin-right: 1px; } -.debug-pane .debug-call-stack .monaco-list-row:hover .monaco-action-bar { +.debug-pane .monaco-list-row:hover .monaco-action-bar { display: initial; } -.debug-pane .debug-call-stack .session .codicon { +.debug-pane .session .codicon { line-height: 22px; margin-right: 2px; } -.monaco-workbench .debug-pane .debug-call-stack .monaco-action-bar .action-item > .action-label { +.monaco-workbench .debug-pane .monaco-action-bar .action-item > .action-label { width: 16px; height: 100%; line-height: 22px; @@ -259,11 +259,11 @@ font-family: var(--monaco-monospace-font); } -.debug-pane .monaco-inputbox > .wrapper { +.debug-pane .monaco-inputbox > .ibwrapper { height: 19px; } -.debug-pane .monaco-inputbox > .wrapper > .input { +.debug-pane .monaco-inputbox > .ibwrapper > .input { padding: 0px; color: initial; } @@ -319,7 +319,7 @@ } .debug-pane .debug-breakpoints .breakpoint > .file-path, -.debug-pane .debug-breakpoints .breakpoint.exception > .condition { +.debug-pane .debug-breakpoints .breakpoint > .condition { opacity: 0.7; margin-left: 0.9em; flex: 1; diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index 04de3a219..d48c7b775 100644 --- a/src/vs/workbench/contrib/debug/browser/media/repl.css +++ b/src/vs/workbench/contrib/debug/browser/media/repl.css @@ -17,6 +17,10 @@ white-space: pre; } +.monaco-workbench .repl .repl-tree .monaco-tl-contents .expression { + font-family: var(--vscode-repl-font-family); +} + .monaco-workbench .repl .repl-tree.word-wrap .monaco-tl-contents { /* Wrap words but also do not trim whitespace #6275 */ word-wrap: break-word; diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index a70036599..19e9bc3b6 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -154,6 +154,10 @@ export class RawDebugSession implements IDisposable { case 'invalidated': this._onDidInvalidated.fire(event as DebugProtocol.InvalidatedEvent); break; + case 'process': + break; + case 'module': + break; default: this._onDidCustomEvent.fire(event); break; diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 74467fa80..1afbbaf12 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/repl'; import { URI as uri } from 'vs/base/common/uri'; -import { IAction, IActionViewItem, Action, Separator } from 'vs/base/common/actions'; +import { IAction, IActionViewItem } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -14,11 +14,11 @@ import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; import { ITextModel } from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; -import { registerEditorAction, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; +import { registerEditorAction, EditorAction } from 'vs/editor/browser/editorExtensions'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextKeyService, IContextKey, ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -26,7 +26,7 @@ import { memoize } from 'vs/base/common/decorators'; import { dispose, IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { IDebugService, DEBUG_SCHEME, CONTEXT_IN_DEBUG_REPL, IDebugSession, State, IReplElement, IDebugConfiguration, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, DEBUG_SCHEME, CONTEXT_IN_DEBUG_REPL, IDebugSession, State, IReplElement, IDebugConfiguration, REPL_VIEW_ID, CONTEXT_MULTI_SESSION_REPL, CONTEXT_DEBUG_STATE, getStateLabel } from 'vs/workbench/contrib/debug/common/debug'; import { HistoryNavigator } from 'vs/base/common/history'; import { IHistoryNavigationWidget } from 'vs/base/browser/history'; import { createAndBindHistoryNavigationWidgetScopedContextKeyService } from 'vs/platform/browser/contextScopedHistoryWidget'; @@ -50,7 +50,7 @@ import { FuzzyScore } from 'vs/base/common/filters'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ReplDelegate, ReplVariablesRenderer, ReplSimpleElementsRenderer, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplRawObjectsRenderer, ReplDataSource, ReplAccessibilityProvider, ReplGroupRenderer } from 'vs/workbench/contrib/debug/browser/replViewer'; import { localize } from 'vs/nls'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -60,6 +60,8 @@ import { EDITOR_FONT_DEFAULTS, EditorOption } from 'vs/editor/common/config/edit import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; import { ReplFilter, ReplFilterState, ReplFilterActionViewItem } from 'vs/workbench/contrib/debug/browser/replFilter'; import { debugConsoleClearAll, debugConsoleEvaluationPrompt } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { registerAction2, MenuId, Action2, IMenuService, IMenu } from 'vs/platform/actions/common/actions'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; const $ = dom.$; @@ -73,17 +75,19 @@ function revealLastElement(tree: WorkbenchAsyncDataTree) { } const sessionsToIgnore = new Set(); +const identityProvider = { getId: (element: IReplElement) => element.getId() }; export class Repl extends ViewPane implements IHistoryNavigationWidget { declare readonly _serviceBrand: undefined; - private static readonly REFRESH_DELAY = 100; // delay in ms to refresh the repl for new elements to show + private static readonly REFRESH_DELAY = 50; // delay in ms to refresh the repl for new elements to show private static readonly URI = uri.parse(`${DEBUG_SCHEME}:replinput`); private history: HistoryNavigator; private tree!: WorkbenchAsyncDataTree; private replDelegate!: ReplDelegate; private container!: HTMLElement; + private treeContainer!: HTMLElement; private replInput!: CodeEditorWidget; private replInputContainer!: HTMLElement; private dimension!: dom.Dimension; @@ -98,6 +102,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { private filter: ReplFilter; private filterState: ReplFilterState; private filterActionViewItem: ReplFilterActionViewItem | undefined; + private multiSessionRepl: IContextKey; + private menu: IMenu; constructor( options: IViewPaneOptions, @@ -112,17 +118,21 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService, - @IClipboardService private readonly clipboardService: IClipboardService, @IEditorService private readonly editorService: IEditorService, @IKeybindingService keybindingService: IKeybindingService, @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, + @IMenuService menuService: IMenuService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.menu = menuService.createMenu(MenuId.DebugConsoleContext, contextKeyService); + this._register(this.menu); this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); this.filter = new ReplFilter(); this.filterState = new ReplFilterState(this); + this.multiSessionRepl = CONTEXT_MULTI_SESSION_REPL.bindTo(contextKeyService); + this.multiSessionRepl.set(this.isMultiSessionView); codeEditorService.registerDecorationType(DECORATION_KEY, {}); this.registerListeners(); @@ -207,7 +217,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { if (!input || input.state === State.Inactive) { await this.selectSession(newSession); } - this.updateActions(); + this.multiSessionRepl.set(this.isMultiSessionView); })); this._register(this.themeService.onDidColorThemeChange(() => { this.refreshReplElements(false); @@ -224,10 +234,16 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.replInput.setModel(this.model); this.updateInputDecoration(); this.refreshReplElements(true); + this.layoutBody(this.dimension.height, this.dimension.width); } })); this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('debug.console.lineHeight') || e.affectsConfiguration('debug.console.fontSize') || e.affectsConfiguration('debug.console.fontFamily')) { + if (e.affectsConfiguration('debug.console.wordWrap')) { + this.tree.dispose(); + this.treeContainer.innerText = ''; + dom.clearNode(this.treeContainer); + this.createReplTree(); + } else if (e.affectsConfiguration('debug.console.lineHeight') || e.affectsConfiguration('debug.console.fontSize') || e.affectsConfiguration('debug.console.fontFamily')) { this.onDidStyleChange(); } })); @@ -305,7 +321,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { if (this.styleElement) { const debugConsole = this.configurationService.getValue('debug').console; const fontSize = debugConsole.fontSize; - const fontFamily = debugConsole.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : `'${debugConsole.fontFamily}'`; + const fontFamily = debugConsole.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : `${debugConsole.fontFamily}`; const lineHeight = debugConsole.lineHeight ? `${debugConsole.lineHeight}px` : '1.4em'; const backgroundColor = this.themeService.getColorTheme().getColor(this.getBackgroundColor()); @@ -321,7 +337,6 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.styleElement.textContent = ` .repl .repl-tree .expression { font-size: ${fontSize}px; - font-family: ${fontFamily}; } .repl .repl-tree .expression { @@ -340,6 +355,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { background-color: ${backgroundColor}; } `; + this.container.style.setProperty(`--vscode-repl-font-family`, fontFamily); this.tree.rerender(); @@ -397,7 +413,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { // Ignore inactive sessions which got cleared - so they are not shown any more sessionsToIgnore.add(session); await this.selectSession(); - this.updateActions(); + this.multiSessionRepl.set(this.isMultiSessionView); } } this.replInput.focus(); @@ -455,14 +471,22 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.replInput.layout({ width: width - 30, height: replInputHeight }); } + collapseAll(): void { + this.tree.collapseAll(); + } + + getReplInput(): CodeEditorWidget { + return this.replInput; + } + focus(): void { setTimeout(() => this.replInput.focus(), 0); } getActionViewItem(action: IAction): IActionViewItem | undefined { - if (action.id === SelectReplAction.ID) { + if (action.id === selectReplCommandId) { const session = (this.tree ? this.tree.getInput() : undefined) ?? this.debugService.getViewModel().focusedSession; - return this.instantiationService.createInstance(SelectReplActionViewItem, this.selectReplAction, session); + return this.instantiationService.createInstance(SelectReplActionViewItem, action, session); } else if (action.id === FILTER_ACTION_ID) { const filterHistory = JSON.parse(this.storageService.get(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')) as string[]; this.filterActionViewItem = this.instantiationService.createInstance(ReplFilterActionViewItem, action, @@ -473,29 +497,11 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { return super.getActionViewItem(action); } - getActions(): IAction[] { - const result: IAction[] = []; - result.push(new Action(FILTER_ACTION_ID)); - if (this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s)).length > 1) { - result.push(this.selectReplAction); - } - result.push(this.clearReplAction); - - result.forEach(a => this._register(a)); - - return result; + private get isMultiSessionView(): boolean { + return this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s)).length > 1; } // --- Cached locals - @memoize - private get selectReplAction(): SelectReplAction { - return this.instantiationService.createInstance(SelectReplAction, SelectReplAction.ID, SelectReplAction.LABEL); - } - - @memoize - private get clearReplAction(): ClearReplAction { - return this.instantiationService.createInstance(ClearReplAction, ClearReplAction.ID, ClearReplAction.LABEL); - } @memoize private get refreshScheduler(): RunOnceScheduler { @@ -506,7 +512,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { } const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight; - await this.tree.updateChildren(); + await this.tree.updateChildren(undefined, true, false, { diffIdentityProvider: identityProvider }); const session = this.tree.getInput(); if (session) { @@ -541,25 +547,27 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { protected renderBody(parent: HTMLElement): void { super.renderBody(parent); - this.container = dom.append(parent, $('.repl')); - const treeContainer = dom.append(this.container, $(`.repl-tree.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); + this.treeContainer = dom.append(this.container, $(`.repl-tree.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); this.createReplInput(this.container); + this.createReplTree(); + } + private createReplTree(): void { this.replDelegate = new ReplDelegate(this.configurationService); const wordWrap = this.configurationService.getValue('debug').console.wordWrap; - treeContainer.classList.toggle('word-wrap', wordWrap); + this.treeContainer.classList.toggle('word-wrap', wordWrap); const linkDetector = this.instantiationService.createInstance(LinkDetector); this.tree = >this.instantiationService.createInstance( WorkbenchAsyncDataTree, 'DebugRepl', - treeContainer, + this.treeContainer, this.replDelegate, [ this.instantiationService.createInstance(ReplVariablesRenderer, linkDetector), this.instantiationService.createInstance(ReplSimpleElementsRenderer, linkDetector), new ReplEvaluationInputsRenderer(), - new ReplGroupRenderer(), + this.instantiationService.createInstance(ReplGroupRenderer, linkDetector), new ReplEvaluationResultsRenderer(linkDetector), new ReplRawObjectsRenderer(linkDetector), ], @@ -568,7 +576,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { { filter: this.filter, accessibilityProvider: new ReplAccessibilityProvider(), - identityProvider: { getId: (element: IReplElement) => element.getId() }, + identityProvider, mouseSupport: false, keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IReplElement) => e }, horizontalScrolling: !wordWrap, @@ -598,7 +606,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.replInputContainer = dom.append(container, $('.repl-input-wrapper')); dom.append(this.replInputContainer, $('.repl-input-chevron' + ThemeIcon.asCSSSelector(debugConsoleEvaluationPrompt))); - const { scopedContextKeyService, historyNavigationEnablement } = createAndBindHistoryNavigationWidgetScopedContextKeyService(this.contextKeyService, { target: this.replInputContainer, historyNavigator: this }); + const { scopedContextKeyService, historyNavigationEnablement } = createAndBindHistoryNavigationWidgetScopedContextKeyService(this.contextKeyService, { target: container, historyNavigator: this }); this.historyNavigationEnablement = historyNavigationEnablement; this._register(scopedContextKeyService); CONTEXT_IN_DEBUG_REPL.bindTo(scopedContextKeyService).set(true); @@ -629,44 +637,12 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { private onContextMenu(e: ITreeContextMenuEvent): void { const actions: IAction[] = []; - actions.push(new Action('debug.replCopy', localize('copy', "Copy"), undefined, true, async () => { - const nativeSelection = window.getSelection(); - if (nativeSelection) { - await this.clipboardService.writeText(nativeSelection.toString()); - } - return Promise.resolve(); - })); - actions.push(new Action('workbench.debug.action.copyAll', localize('copyAll', "Copy All"), undefined, true, async () => { - await this.clipboardService.writeText(this.getVisibleContent()); - return Promise.resolve(); - })); - actions.push(new Action('debug.replPaste', localize('paste', "Paste"), undefined, this.debugService.state !== State.Inactive, async () => { - const clipboardText = await this.clipboardService.readText(); - if (clipboardText) { - this.replInput.setValue(this.replInput.getValue().concat(clipboardText)); - this.replInput.focus(); - const model = this.replInput.getModel(); - const lineNumber = model ? model.getLineCount() : 0; - const column = model?.getLineMaxColumn(lineNumber); - if (typeof lineNumber === 'number' && typeof column === 'number') { - this.replInput.setPosition({ lineNumber, column }); - } - } - })); - actions.push(new Separator()); - actions.push(new Action('debug.collapseRepl', localize('collapse', "Collapse All"), undefined, true, () => { - this.tree.collapseAll(); - this.replInput.focus(); - return Promise.resolve(); - })); - actions.push(new Separator()); - actions.push(this.clearReplAction); - + const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: e.element, shouldForwardArgs: false }, actions); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => actions, getActionsContext: () => e.element, - onHide: () => dispose(actions) + onHide: () => dispose(actionsDisposable) }); } @@ -825,48 +801,183 @@ class SelectReplActionViewItem extends FocusSessionActionViewItem { } } -class SelectReplAction extends Action { - - static readonly ID = 'workbench.action.debug.selectRepl'; - static readonly LABEL = localize('selectRepl', "Select Debug Console"); - - constructor(id: string, label: string, - @IDebugService private readonly debugService: IDebugService, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label); - } - - async run(session: IDebugSession): Promise { - // If session is already the focused session we need to manualy update the tree since view model will not send a focused change event - if (session && session.state !== State.Inactive && session !== this.debugService.getViewModel().focusedSession) { - await this.debugService.focusStackFrame(undefined, undefined, session, true); - } else { - const repl = getReplView(this.viewsService); - if (repl) { - await repl.selectSession(session); - } - } - } -} - -export class ClearReplAction extends Action { - static readonly ID = 'workbench.debug.panel.action.clearReplAction'; - static readonly LABEL = localize('clearRepl', "Clear Console"); - - constructor(id: string, label: string, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(debugConsoleClearAll)); - } - - async run(): Promise { - const view = await this.viewsService.openView(REPL_VIEW_ID) as Repl; - await view.clearRepl(); - aria.status(localize('debugConsoleCleared', "Debug console was cleared")); - } -} - function getReplView(viewsService: IViewsService): Repl | undefined { return viewsService.getActiveViewWithId(REPL_VIEW_ID) as Repl ?? undefined; } + +registerAction2(class extends Action2 { + constructor() { + super({ + id: FILTER_ACTION_ID, + title: localize('filter', "Filter"), + f1: true, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', REPL_VIEW_ID), + order: 10 + } + }); + } + + run(_accessor: ServicesAccessor) { + // noop this action is just a placeholder for the filter action view item + } +}); + +const selectReplCommandId = 'workbench.action.debug.selectRepl'; +registerAction2(class extends ViewAction { + constructor() { + super({ + id: selectReplCommandId, + viewId: REPL_VIEW_ID, + title: localize('selectRepl', "Select Debug Console"), + f1: true, + icon: debugConsoleClearAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', REPL_VIEW_ID), CONTEXT_MULTI_SESSION_REPL), + order: 20 + } + }); + } + + async runInView(accessor: ServicesAccessor, view: Repl, session: IDebugSession | undefined) { + const debugService = accessor.get(IDebugService); + // If session is already the focused session we need to manualy update the tree since view model will not send a focused change event + if (session && session.state !== State.Inactive && session !== debugService.getViewModel().focusedSession) { + if (session.state !== State.Stopped) { + // Focus child session instead if it is stopped #112595 + const stopppedChildSession = debugService.getModel().getSessions().find(s => s.parentSession === session); + if (stopppedChildSession) { + session = stopppedChildSession; + } + } + await debugService.focusStackFrame(undefined, undefined, session, true); + } else { + await view.selectSession(session); + } + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'workbench.debug.panel.action.clearReplAction', + viewId: REPL_VIEW_ID, + title: localize('clearRepl', "Clear Console"), + f1: true, + icon: debugConsoleClearAll, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', REPL_VIEW_ID), + order: 30 + }, { + id: MenuId.DebugConsoleContext, + group: 'z_commands', + order: 20 + }] + }); + } + + runInView(_accessor: ServicesAccessor, view: Repl): void { + view.clearRepl(); + aria.status(localize('debugConsoleCleared', "Debug console was cleared")); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.collapseRepl', + title: localize('collapse', "Collapse All"), + viewId: REPL_VIEW_ID, + menu: { + id: MenuId.DebugConsoleContext, + group: 'z_commands', + order: 10 + } + }); + } + + runInView(_accessor: ServicesAccessor, view: Repl): void { + view.collapseAll(); + view.focus(); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.replPaste', + title: localize('paste', "Paste"), + viewId: REPL_VIEW_ID, + precondition: CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Inactive)), + menu: { + id: MenuId.DebugConsoleContext, + group: '2_cutcopypaste', + order: 30 + } + }); + } + + async runInView(accessor: ServicesAccessor, view: Repl): Promise { + const clipboardService = accessor.get(IClipboardService); + const clipboardText = await clipboardService.readText(); + if (clipboardText) { + const replInput = view.getReplInput(); + replInput.setValue(replInput.getValue().concat(clipboardText)); + view.focus(); + const model = replInput.getModel(); + const lineNumber = model ? model.getLineCount() : 0; + const column = model?.getLineMaxColumn(lineNumber); + if (typeof lineNumber === 'number' && typeof column === 'number') { + replInput.setPosition({ lineNumber, column }); + } + } + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'workbench.debug.action.copyAll', + title: localize('copyAll', "Copy All"), + viewId: REPL_VIEW_ID, + menu: { + id: MenuId.DebugConsoleContext, + group: '2_cutcopypaste', + order: 20 + } + }); + } + + async runInView(accessor: ServicesAccessor, view: Repl): Promise { + const clipboardService = accessor.get(IClipboardService); + await clipboardService.writeText(view.getVisibleContent()); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'debug.replCopy', + title: localize('copy', "Copy"), + menu: { + id: MenuId.DebugConsoleContext, + group: '2_cutcopypaste', + order: 10 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const clipboardService = accessor.get(IClipboardService); + const nativeSelection = window.getSelection(); + if (nativeSelection) { + await clipboardService.writeText(nativeSelection.toString()); + } + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index 0d432417f..937976c63 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -34,7 +34,7 @@ interface IReplEvaluationInputTemplateData { } interface IReplGroupTemplateData { - label: HighlightedLabel; + label: HTMLElement; } interface IReplEvaluationResultTemplateData { @@ -87,22 +87,28 @@ export class ReplEvaluationInputsRenderer implements ITreeRenderer { static readonly ID = 'replGroup'; + constructor( + private readonly linkDetector: LinkDetector, + @IThemeService private readonly themeService: IThemeService + ) { } + get templateId(): string { return ReplGroupRenderer.ID; } - renderTemplate(container: HTMLElement): IReplEvaluationInputTemplateData { - const input = dom.append(container, $('.expression')); - const label = new HighlightedLabel(input, false); + renderTemplate(container: HTMLElement): IReplGroupTemplateData { + const label = dom.append(container, $('.expression')); return { label }; } renderElement(element: ITreeNode, _index: number, templateData: IReplGroupTemplateData): void { const replGroup = element.element; - templateData.label.set(replGroup.name, createMatches(element.filterData)); + dom.clearNode(templateData.label); + const result = handleANSIOutput(replGroup.name, this.linkDetector, this.themeService, undefined); + templateData.label.appendChild(result); } - disposeTemplate(_templateData: IReplEvaluationInputTemplateData): void { + disposeTemplate(_templateData: IReplGroupTemplateData): void { // noop } } @@ -300,15 +306,15 @@ export class ReplDelegate extends CachedListVirtualDelegate { protected estimateHeight(element: IReplElement, ignoreValueLength = false): number { const config = this.configurationService.getValue('debug'); - const rowHeight = Math.ceil(1.4 * config.console.fontSize); + const rowHeight = Math.ceil(1.3 * config.console.fontSize); const countNumberOfLines = (str: string) => Math.max(1, (str && str.match(/\r\n|\n/g) || []).length); const hasValue = (e: any): e is { value: string } => typeof e.value === 'string'; // Calculate a rough overestimation for the height - // For every 30 characters increase the number of lines needed - if (hasValue(element)) { + // For every 70 characters increase the number of lines needed beyond the first + if (hasValue(element) && !(element instanceof Variable)) { let value = element.value; - let valueRows = countNumberOfLines(value) + (ignoreValueLength ? 0 : Math.floor(value.length / 30)); + let valueRows = countNumberOfLines(value) + (ignoreValueLength ? 0 : Math.floor(value.length / 70)); return valueRows * rowHeight; } @@ -338,6 +344,10 @@ export class ReplDelegate extends CachedListVirtualDelegate { } hasDynamicHeight(element: IReplElement): boolean { + if (element instanceof Variable) { + // Variables should always be in one line #111843 + return false; + } // Empty elements should not have dynamic height since they will be invisible return element.toString().length > 0; } @@ -383,7 +393,8 @@ export class ReplAccessibilityProvider implements IListAccessibilityProvider 1 ? localize('occurred', ", occured {0} times", element.count) : ''); + return element.value + (element instanceof SimpleReplElement && element.count > 1 ? localize({ key: 'occurred', comment: ['Front will the value of the debug console element. Placeholder will be replaced by a number which represents occurrance count.'] }, + ", occured {0} times", element.count) : ''); } if (element instanceof RawObjectReplElement) { return localize('replRawObjectAriaLabel', "Debug console variable {0}, value {1}", element.name, element.value); diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 7c8acdc41..665894a0b 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -3,20 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; -import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, IDataBreakpointInfoResponse, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT } from 'vs/workbench/contrib/debug/common/debug'; -import { Variable, Scope, ErrorScope, StackFrame } from 'vs/workbench/contrib/debug/common/debugModel'; +import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, IDataBreakpointInfoResponse, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { Variable, Scope, ErrorScope, StackFrame, Expression } from 'vs/workbench/contrib/debug/common/debugModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { renderViewTree, renderVariable, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IAction } from 'vs/base/common/actions'; -import { CopyValueAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, ITreeMouseEvent, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; @@ -26,17 +23,19 @@ import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { dispose } from 'vs/base/common/lifecycle'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { withUndefinedAsNull } from 'vs/base/common/types'; -import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions'; +import { IMenuService, IMenu, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { debugCollapseAll } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { localize } from 'vs/nls'; +import { Codicon } from 'vs/base/common/codicons'; +import { coalesce } from 'vs/base/common/arrays'; const $ = dom.$; let forgetScopes = true; @@ -179,10 +178,6 @@ export class VariablesView extends ViewPane { })); } - getActions(): IAction[] { - return [new CollapseAction(() => this.tree, true, 'explorer-action ' + ThemeIcon.asClassName(debugCollapseAll))]; - } - layoutBody(width: number, height: number): void { super.layoutBody(height, width); this.tree.layout(width, height); @@ -192,6 +187,10 @@ export class VariablesView extends ViewPane { this.tree.domFocus(); } + collapseAll(): void { + this.tree.collapseAll(); + } + private onMouseDblClick(e: ITreeMouseEvent): void { const session = this.debugService.getViewModel().focusedSession; if (session && e.element instanceof Variable && session.capabilities.supportsSetVariable) { @@ -345,7 +344,7 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { const variable = expression; return { initialValue: expression.value, - ariaLabel: nls.localize('variableValueAriaLabel', "Type new variable value"), + ariaLabel: localize('variableValueAriaLabel', "Type new variable value"), validationOptions: { validation: () => variable.errorMessage ? ({ content: variable.errorMessage }) : null }, @@ -368,15 +367,15 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { class VariablesAccessibilityProvider implements IListAccessibilityProvider { getWidgetAriaLabel(): string { - return nls.localize('variablesAriaTreeLabel', "Debug Variables"); + return localize('variablesAriaTreeLabel', "Debug Variables"); } getAriaLabel(element: IExpression | IScope): string | null { if (element instanceof Scope) { - return nls.localize('variableScopeAriaLabel', "Scope {0}", element.name); + return localize('variableScopeAriaLabel', "Scope {0}", element.name); } if (element instanceof Variable) { - return nls.localize({ key: 'variableAriaLabel', comment: ['Placeholders are variable name and variable value respectivly. They should not be translated.'] }, "{0}, value {1}", element.name, element.value); + return localize({ key: 'variableAriaLabel', comment: ['Placeholders are variable name and variable value respectivly. They should not be translated.'] }, "{0}, value {1}", element.name, element.value); } return null; @@ -392,14 +391,40 @@ CommandsRegistry.registerCommand({ } }); -export const COPY_VALUE_ID = 'debug.copyValue'; +export const COPY_VALUE_ID = 'workbench.debug.viewlet.action.copyValue'; CommandsRegistry.registerCommand({ id: COPY_VALUE_ID, - handler: async (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - if (variableInternalContext) { - const action = instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variableInternalContext, 'variables'); - await action.run(); + handler: async (accessor: ServicesAccessor, arg: Variable | Expression | unknown, ctx?: (Variable | Expression)[]) => { + const debugService = accessor.get(IDebugService); + const clipboardService = accessor.get(IClipboardService); + let elementContext = ''; + let elements: (Variable | Expression)[]; + if (arg instanceof Variable || arg instanceof Expression) { + elementContext = 'watch'; + elements = ctx ? ctx : []; + } else { + elementContext = 'variables'; + elements = variableInternalContext ? [variableInternalContext] : []; + } + + const stackFrame = debugService.getViewModel().focusedStackFrame; + const session = debugService.getViewModel().focusedSession; + if (!stackFrame || !session || elements.length === 0) { + return; + } + + const evalContext = session.capabilities.supportsClipboardContext ? 'clipboard' : elementContext; + const toEvaluate = elements.map(element => element instanceof Variable ? (element.evaluateName || element.value) : element.name); + + try { + const evaluations = await Promise.all(toEvaluate.map(expr => session.evaluate(expr, stackFrame.frameId, evalContext))); + const result = coalesce(evaluations).map(evaluation => evaluation.body.result); + if (result.length) { + clipboardService.writeText(result.join('\n')); + } + } catch (e) { + const result = elements.map(element => element.value); + clipboardService.writeText(result.join('\n')); } } }); @@ -433,3 +458,23 @@ CommandsRegistry.registerCommand({ } }); +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'variables.collapse', + viewId: VARIABLES_VIEW_ID, + title: localize('collapse', "Collapse All"), + f1: false, + icon: Codicon.collapseAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VARIABLES_VIEW_ID) + } + }); + } + + runInView(_accessor: ServicesAccessor, view: VariablesView) { + view.collapseAll(); + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 2e18414c9..23ee7babf 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -3,20 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, IExpression, CONTEXT_WATCH_EXPRESSIONS_FOCUSED } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IExpression, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, WATCH_VIEW_ID, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_ITEM_TYPE } from 'vs/workbench/contrib/debug/common/debug'; import { Expression, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; -import { AddWatchExpressionAction, RemoveAllWatchExpressionsAction, CopyValueAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IAction, Action, Separator } from 'vs/base/common/actions'; +import { IAction } from 'vs/base/common/actions'; import { renderExpressionValue, renderViewTree, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; @@ -26,13 +23,17 @@ import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { FuzzyScore } from 'vs/base/common/filters'; import { IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyEqualsExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { dispose } from 'vs/base/common/lifecycle'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { debugCollapseAll } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { watchExpressionsRemoveAll, watchExpressionsAdd } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { registerAction2, MenuId, Action2, IMenuService, IMenu } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { Codicon } from 'vs/base/common/codicons'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; let ignoreViewUpdates = false; @@ -43,6 +44,9 @@ export class WatchExpressionsView extends ViewPane { private watchExpressionsUpdatedScheduler: RunOnceScheduler; private needsRefresh = false; private tree!: WorkbenchAsyncDataTree; + private watchExpressionsExist: IContextKey; + private watchItemType: IContextKey; + private menu: IMenu; constructor( options: IViewletViewOptions, @@ -56,13 +60,19 @@ export class WatchExpressionsView extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, + @IMenuService menuService: IMenuService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.menu = menuService.createMenu(MenuId.DebugWatchContext, contextKeyService); + this._register(this.menu); this.watchExpressionsUpdatedScheduler = new RunOnceScheduler(() => { this.needsRefresh = false; this.tree.updateChildren(); }, 50); + this.watchExpressionsExist = CONTEXT_WATCH_EXPRESSIONS_EXIST.bindTo(contextKeyService); + this.watchExpressionsExist.set(this.debugService.getModel().getWatchExpressions().length > 0); + this.watchItemType = CONTEXT_WATCH_ITEM_TYPE.bindTo(contextKeyService); } renderBody(container: HTMLElement): void { @@ -98,6 +108,7 @@ export class WatchExpressionsView extends ViewPane { this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e))); this._register(this.debugService.getModel().onDidChangeWatchExpressions(async we => { + this.watchExpressionsExist.set(this.debugService.getModel().getWatchExpressions().length > 0); if (!this.isBodyVisible()) { this.needsRefresh = true; } else { @@ -141,7 +152,10 @@ export class WatchExpressionsView extends ViewPane { this.tree.updateOptions({ horizontalScrolling: false }); } - this.tree.rerender(e); + if (e.name) { + // Only rerender if the input is already done since otherwise the tree is not yet aware of the new element + this.tree.rerender(e); + } } else if (!e && horizontalScrolling !== undefined) { this.tree.updateOptions({ horizontalScrolling: horizontalScrolling }); horizontalScrolling = undefined; @@ -158,12 +172,8 @@ export class WatchExpressionsView extends ViewPane { this.tree.domFocus(); } - getActions(): IAction[] { - return [ - new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService), - new CollapseAction(() => this.tree, true, 'explorer-action ' + ThemeIcon.asClassName(debugCollapseAll)), - new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService) - ]; + collapseAll(): void { + this.tree.collapseAll(); } private onMouseDblClick(e: ITreeMouseEvent): void { @@ -184,42 +194,17 @@ export class WatchExpressionsView extends ViewPane { private onContextMenu(e: ITreeContextMenuEvent): void { const element = e.element; - const anchor = e.anchor; - if (!anchor) { - return; - } + const selection = this.tree.getSelection(); + + this.watchItemType.set(element instanceof Expression ? 'expression' : element instanceof Variable ? 'variable' : undefined); const actions: IAction[] = []; - - if (element instanceof Expression) { - const expression = element; - actions.push(new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService)); - actions.push(new Action('debug.editWatchExpression', nls.localize('editWatchExpression', "Edit Expression"), undefined, true, () => { - this.debugService.getViewModel().setSelectedExpression(expression); - return Promise.resolve(); - })); - actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, expression, 'watch')); - actions.push(new Separator()); - - actions.push(new Action('debug.removeWatchExpression', nls.localize('removeWatchExpression', "Remove Expression"), undefined, true, () => { - this.debugService.removeWatchExpressions(expression.getId()); - return Promise.resolve(); - })); - actions.push(new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService)); - } else { - actions.push(new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService)); - if (element instanceof Variable) { - const variable = element as Variable; - actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variable, 'watch')); - actions.push(new Separator()); - } - actions.push(new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService)); - } + const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: element, shouldForwardArgs: true }, actions); this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, + getAnchor: () => e.anchor, getActions: () => actions, - getActionsContext: () => element, - onHide: () => dispose(actions) + getActionsContext: () => element && selection.includes(element) ? selection : element ? [element] : [], + onHide: () => dispose(actionsDisposable) }); } } @@ -287,8 +272,8 @@ export class WatchExpressionsRenderer extends AbstractExpressionsRenderer { protected getInputBoxOptions(expression: IExpression): IInputBoxOptions { return { initialValue: expression.name ? expression.name : '', - ariaLabel: nls.localize('watchExpressionInputAriaLabel', "Type watch expression"), - placeholder: nls.localize('watchExpressionPlaceholder', "Expression to watch"), + ariaLabel: localize('watchExpressionInputAriaLabel', "Type watch expression"), + placeholder: localize('watchExpressionPlaceholder', "Expression to watch"), onFinish: (value: string, success: boolean) => { if (success && value) { this.debugService.renameWatchExpression(expression.getId(), value); @@ -306,16 +291,16 @@ export class WatchExpressionsRenderer extends AbstractExpressionsRenderer { class WatchExpressionsAccessibilityProvider implements IListAccessibilityProvider { getWidgetAriaLabel(): string { - return nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'watchAriaTreeLabel' }, "Debug Watch Expressions"); + return localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'watchAriaTreeLabel' }, "Debug Watch Expressions"); } getAriaLabel(element: IExpression): string { if (element instanceof Expression) { - return nls.localize('watchExpressionAriaLabel', "{0}, value {1}", (element).name, (element).value); + return localize('watchExpressionAriaLabel', "{0}, value {1}", (element).name, (element).value); } // Variable - return nls.localize('watchVariableAriaLabel', "{0}, value {1}", (element).name, (element).value); + return localize('watchVariableAriaLabel', "{0}, value {1}", (element).name, (element).value); } } @@ -359,3 +344,75 @@ class WatchExpressionsDragAndDrop implements ITreeDragAndDrop { this.debugService.moveWatchExpression(draggedElement.getId(), position); } } + +registerAction2(class Collapse extends ViewAction { + constructor() { + super({ + id: 'watch.collapse', + viewId: WATCH_VIEW_ID, + title: localize('collapse', "Collapse All"), + f1: false, + icon: Codicon.collapseAll, + precondition: CONTEXT_WATCH_EXPRESSIONS_EXIST, + menu: { + id: MenuId.ViewTitle, + order: 30, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', WATCH_VIEW_ID) + } + }); + } + + runInView(_accessor: ServicesAccessor, view: WatchExpressionsView) { + view.collapseAll(); + } +}); + +export const ADD_WATCH_ID = 'workbench.debug.viewlet.action.addWatchExpression'; // Use old and long id for backwards compatibility +export const ADD_WATCH_LABEL = localize('addWatchExpression', "Add Expression"); + +registerAction2(class AddWatchExpressionAction extends Action2 { + constructor() { + super({ + id: ADD_WATCH_ID, + title: ADD_WATCH_LABEL, + f1: false, + icon: watchExpressionsAdd, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', WATCH_VIEW_ID) + } + }); + } + + run(accessor: ServicesAccessor): void { + const debugService = accessor.get(IDebugService); + debugService.addWatchExpression(); + } +}); + +export const REMOVE_WATCH_EXPRESSIONS_COMMAND_ID = 'workbench.debug.viewlet.action.removeAllWatchExpressions'; +export const REMOVE_WATCH_EXPRESSIONS_LABEL = localize('removeAllWatchExpressions', "Remove All Expressions"); +registerAction2(class RemoveAllWatchExpressionsAction extends Action2 { + constructor() { + super({ + id: REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, // Use old and long id for backwards compatibility + title: REMOVE_WATCH_EXPRESSIONS_LABEL, + f1: false, + icon: watchExpressionsRemoveAll, + precondition: CONTEXT_WATCH_EXPRESSIONS_EXIST, + menu: { + id: MenuId.ViewTitle, + order: 20, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', WATCH_VIEW_ID) + } + }); + } + + run(accessor: ServicesAccessor): void { + const debugService = accessor.get(IDebugService); + debugService.removeWatchExpressions(); + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/welcomeView.ts b/src/vs/workbench/contrib/debug/browser/welcomeView.ts index c88b28e8b..e33671e73 100644 --- a/src/vs/workbench/contrib/debug/browser/welcomeView.ts +++ b/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -10,10 +10,9 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService, RawContextKey, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; -import { StartAction, ConfigureAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IDebugService, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IViewDescriptorService, IViewsRegistry, Extensions, ViewContentGroups } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -25,6 +24,7 @@ import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { SELECT_AND_START_ID, DEBUG_CONFIGURE_COMMAND_ID, DEBUG_START_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; const debugStartLanguageKey = 'debugStartLanguage'; const CONTEXT_DEBUG_START_LANGUAGE = new RawContextKey(debugStartLanguageKey, undefined); @@ -96,7 +96,7 @@ export class WelcomeView extends ViewPane { })); setContextKey(); - const debugKeybinding = this.keybindingService.lookupKeybinding(StartAction.ID); + const debugKeybinding = this.keybindingService.lookupKeybinding(DEBUG_START_COMMAND_ID); debugKeybindingLabel = debugKeybinding ? ` (${debugKeybinding.getLabel()})` : ''; } @@ -116,14 +116,14 @@ viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { let debugKeybindingLabel = ''; viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'runAndDebugAction', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, - "[Run and Debug{0}](command:{1})", debugKeybindingLabel, StartAction.ID), + "[Run and Debug{0}](command:{1})", debugKeybindingLabel, DEBUG_START_COMMAND_ID), when: CONTEXT_DEBUGGERS_AVAILABLE, group: ViewContentGroups.Debug }); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'detectThenRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, - "[Show](command:{0}) all automatic debug configurations.", SelectAndStartAction.ID), + "[Show](command:{0}) all automatic debug configurations.", SELECT_AND_START_ID), when: CONTEXT_DEBUGGERS_AVAILABLE, group: ViewContentGroups.Debug, order: 10 @@ -131,7 +131,7 @@ viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'customizeRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, - "To customize Run and Debug [create a launch.json file](command:{0}).", ConfigureAction.ID), + "To customize Run and Debug [create a launch.json file](command:{0}).", DEBUG_CONFIGURE_COMMAND_ID), when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, WorkbenchStateContext.notEqualsTo('empty')), group: ViewContentGroups.Debug }); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 8f1715543..c46778c61 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -25,6 +25,7 @@ import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configur import { CancellationToken } from 'vs/base/common/cancellation'; import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; +import { IAction } from 'vs/base/common/actions'; export const VIEWLET_ID = 'workbench.view.debug'; @@ -47,10 +48,14 @@ export const CONTEXT_BREAKPOINT_WIDGET_VISIBLE = new RawContextKey('bre export const CONTEXT_IN_BREAKPOINT_WIDGET = new RawContextKey('inBreakpointWidget', false); export const CONTEXT_BREAKPOINTS_FOCUSED = new RawContextKey('breakpointsFocused', true); export const CONTEXT_WATCH_EXPRESSIONS_FOCUSED = new RawContextKey('watchExpressionsFocused', true); +export const CONTEXT_WATCH_EXPRESSIONS_EXIST = new RawContextKey('watchExpressionsExist', false); export const CONTEXT_VARIABLES_FOCUSED = new RawContextKey('variablesFocused', true); export const CONTEXT_EXPRESSION_SELECTED = new RawContextKey('expressionSelected', false); -export const CONTEXT_BREAKPOINT_SELECTED = new RawContextKey('breakpointSelected', false); +export const CONTEXT_BREAKPOINT_INPUT_FOCUSED = new RawContextKey('breakpointInputFocused', false); export const CONTEXT_CALLSTACK_ITEM_TYPE = new RawContextKey('callStackItemType', undefined); +export const CONTEXT_WATCH_ITEM_TYPE = new RawContextKey('watchItemType', undefined); +export const CONTEXT_BREAKPOINT_ITEM_TYPE = new RawContextKey('breakpointItemType', undefined); +export const CONTEXT_BREAKPOINT_SUPPORTS_CONDITION = new RawContextKey('breakpointSupportsCondition', false); export const CONTEXT_LOADED_SCRIPTS_SUPPORTED = new RawContextKey('loadedScriptsSupported', false); export const CONTEXT_LOADED_SCRIPTS_ITEM_TYPE = new RawContextKey('loadedScriptsItemType', undefined); export const CONTEXT_FOCUSED_SESSION_IS_ATTACH = new RawContextKey('focusedSessionIsAttach', false); @@ -65,6 +70,8 @@ export const CONTEXT_SET_VARIABLE_SUPPORTED = new RawContextKey('debugS export const CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED = new RawContextKey('breakWhenValueChangesSupported', false); export const CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT = new RawContextKey('variableEvaluateNamePresent', false); export const CONTEXT_EXCEPTION_WIDGET_VISIBLE = new RawContextKey('exceptionWidgetVisible', false); +export const CONTEXT_MULTI_SESSION_REPL = new RawContextKey('multiSessionRepl', false); +export const CONTEXT_MULTI_SESSION_DEBUG = new RawContextKey('multiSessionDebug', false); export const EDITOR_CONTRIBUTION_ID = 'editor.contrib.debug'; export const BREAKPOINT_EDITOR_CONTRIBUTION_ID = 'editor.contrib.breakpoint'; @@ -183,6 +190,7 @@ export interface IDebugSession extends ITreeElement { readonly subId: string | undefined; readonly compact: boolean; readonly compoundRoot: DebugCompoundRoot | undefined; + readonly name: string; setSubId(subId: string | undefined): void; @@ -437,9 +445,7 @@ export interface IViewModel extends ITreeElement { readonly focusedStackFrame: IStackFrame | undefined; getSelectedExpression(): IExpression | undefined; - getSelectedBreakpoint(): IFunctionBreakpoint | IExceptionBreakpoint | undefined; setSelectedExpression(expression: IExpression | undefined): void; - setSelectedBreakpoint(functionBreakpoint: IFunctionBreakpoint | IExceptionBreakpoint | undefined): void; updateViews(): void; isMultiSessionView(): boolean; @@ -629,7 +635,6 @@ export interface IDebuggerContribution extends IPlatformSpecificAdapterContribut // supported languages languages?: string[]; - enableBreakpointsFor?: { languageIds?: string[] }; // debug configuration support configurationAttributes?: any; @@ -845,10 +850,10 @@ export interface IDebugService { addFunctionBreakpoint(name?: string, id?: string): void; /** - * Renames an already existing function breakpoint. + * Updates an already existing function breakpoint. * Notifies debug adapter of breakpoint changes. */ - renameFunctionBreakpoint(id: string, newFunctionName: string): Promise; + updateFunctionBreakpoint(id: string, update: { name?: string, hitCondition?: string, condition?: string }): Promise; /** * Removes all function breakpoints. If id is passed only removes the function breakpoint with the passed id. @@ -869,6 +874,8 @@ export interface IDebugService { setExceptionBreakpointCondition(breakpoint: IExceptionBreakpoint, condition: string | undefined): Promise; + setExceptionBreakpoints(data: DebugProtocol.ExceptionBreakpointsFilter[]): void; + /** * Sends all breakpoints to the passed session. * If session is not passed, sends all breakpoints to each session. @@ -947,6 +954,7 @@ export interface IDebugEditorContribution extends editorCommon.IEditorContributi export interface IBreakpointEditorContribution extends editorCommon.IEditorContribution { showBreakpointWidget(lineNumber: number, column: number | undefined, context?: BreakpointWidgetContext): void; closeBreakpointWidget(): void; + getContextMenuActionsAtPosition(lineNumber: number, model: EditorIModel): IAction[]; } // temporary debug helper service diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index b98b3b897..68184d87c 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -423,7 +423,9 @@ export class Thread implements IThread { } getTopStackFrame(): IStackFrame | undefined { - return this.getCallStack().find(sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize')); + const callStack = this.getCallStack(); + const firstAvailableStackFrame = callStack.find(sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize')); + return firstAvailableStackFrame || (callStack.length > 0 ? callStack[0] : undefined); } get stateLabel(): string { @@ -452,6 +454,9 @@ export class Thread implements IThread { this.callStack.splice(start, this.callStack.length - start); } this.callStack = this.callStack.concat(callStack || []); + if (typeof this.stoppedDetails?.totalFrames === 'number' && this.stoppedDetails.totalFrames === this.callStack.length) { + this.reachedEndOfCallStack = true; + } } } @@ -946,6 +951,11 @@ export class DebugModel implements IDebugModel { return true; }); + let i = 1; + while (this.sessions.some(s => s.getLabel() === session.getLabel())) { + session.setName(`${session.configuration.name} ${++i}`); + } + let index = -1; if (session.parentSession) { // Make sure that child sessions are placed after the parent session @@ -1236,10 +1246,18 @@ export class DebugModel implements IDebugModel { return newFunctionBreakpoint; } - renameFunctionBreakpoint(id: string, name: string): void { + updateFunctionBreakpoint(id: string, update: { name?: string, hitCondition?: string, condition?: string }): void { const functionBreakpoint = this.functionBreakpoints.find(fbp => fbp.getId() === id); if (functionBreakpoint) { - functionBreakpoint.name = name; + if (typeof update.name === 'string') { + functionBreakpoint.name = update.name; + } + if (typeof update.condition === 'string') { + functionBreakpoint.condition = update.condition; + } + if (typeof update.hitCondition === 'string') { + functionBreakpoint.hitCondition = update.hitCondition; + } this._onDidChangeBreakpoints.fire({ changed: [functionBreakpoint], sessionOnly: false }); } } diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index f11bab4fb..dad39a583 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -740,7 +740,7 @@ declare module DebugProtocol { /** Reference to the Variable container if the data breakpoint is requested for a child of the container. */ variablesReference?: number; /** The name of the Variable's child to obtain data breakpoint information for. - If variableReference isn’t provided, this can be an expression. + If variablesReference isn’t provided, this can be an expression. */ name: string; } @@ -1012,7 +1012,7 @@ declare module DebugProtocol { /** StackTrace request; value of command field is 'stackTrace'. The request returns a stacktrace from the current execution state of a given thread. - A client can request all stack frames by omitting the startFrame and levels arguments. For performance conscious clients stack frames can be retrieved in a piecemeal way with the startFrame and levels arguments. The response of the stackTrace request may contain a totalFrames property that hints at the total number of frames in the stack. If a client needs this total number upfront, it can issue a request for a single (first) frame and depending on the value of totalFrames decide how to proceed. In any case a client should be prepared to receive less frames than requested, which is an indication that the end of the stack has been reached. + A client can request all stack frames by omitting the startFrame and levels arguments. For performance conscious clients and if the debug adapter's 'supportsDelayedStackTraceLoading' capability is true, stack frames can be retrieved in a piecemeal way with the startFrame and levels arguments. The response of the stackTrace request may contain a totalFrames property that hints at the total number of frames in the stack. If a client needs this total number upfront, it can issue a request for a single (first) frame and depending on the value of totalFrames decide how to proceed. In any case a client should be prepared to receive less frames than requested, which is an indication that the end of the stack has been reached. */ export interface StackTraceRequest extends Request { // command: 'stackTrace'; @@ -1591,7 +1591,7 @@ declare module DebugProtocol { supportsExceptionInfoRequest?: boolean; /** The debug adapter supports the 'terminateDebuggee' attribute on the 'disconnect' request. */ supportTerminateDebuggee?: boolean; - /** The debug adapter supports the delayed loading of parts of the stack, which requires that both the 'startFrame' and 'levels' arguments and the 'totalFrames' result of the 'StackTrace' request are supported. */ + /** The debug adapter supports the delayed loading of parts of the stack, which requires that both the 'startFrame' and 'levels' arguments and an optional 'totalFrames' result of the 'StackTrace' request are supported. */ supportsDelayedStackTraceLoading?: boolean; /** The debug adapter supports the 'loadedSources' request. */ supportsLoadedSourcesRequest?: boolean; @@ -1871,7 +1871,7 @@ declare module DebugProtocol { 'mostDerivedClass': Indicates that the object is the most derived class. 'virtual': Indicates that the object is virtual, that means it is a synthetic object introducedby the adapter for rendering purposes, e.g. an index range for large arrays. - 'dataBreakpoint': Indicates that a data breakpoint is registered for the object. + 'dataBreakpoint': Deprecated: Indicates that a data breakpoint is registered for the object. The 'hasDataBreakpoint' attribute should generally be used instead. etc. */ kind?: 'property' | 'method' | 'class' | 'data' | 'event' | 'baseClass' | 'innerClass' | 'interface' | 'mostDerivedClass' | 'virtual' | 'dataBreakpoint' | string; @@ -1884,9 +1884,10 @@ declare module DebugProtocol { 'hasObjectId': Indicates that the object can have an Object ID created for it. 'canHaveObjectId': Indicates that the object has an Object ID associated with it. 'hasSideEffects': Indicates that the evaluation had side effects. + 'hasDataBreakpoint': Indicates that the object has its value tracked by a data breakpoint. etc. */ - attributes?: ('static' | 'constant' | 'readOnly' | 'rawString' | 'hasObjectId' | 'canHaveObjectId' | 'hasSideEffects' | string)[]; + attributes?: ('static' | 'constant' | 'readOnly' | 'rawString' | 'hasObjectId' | 'canHaveObjectId' | 'hasSideEffects' | 'hasDataBreakpoint' | string)[]; /** Visibility of variable. Before introducing additional values, try to use the listed values. Values: 'public', 'private', 'protected', 'internal', 'final', etc. */ diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index e7541cd89..a17e61185 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, IFunctionBreakpoint, CONTEXT_BREAKPOINT_SELECTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, IExceptionBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG } from 'vs/workbench/contrib/debug/common/debug'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; @@ -16,14 +16,11 @@ export class ViewModel implements IViewModel { private _focusedSession: IDebugSession | undefined; private _focusedThread: IThread | undefined; private selectedExpression: IExpression | undefined; - private selectedBreakpoint: IFunctionBreakpoint | IExceptionBreakpoint | undefined; private readonly _onDidFocusSession = new Emitter(); private readonly _onDidFocusStackFrame = new Emitter<{ stackFrame: IStackFrame | undefined, explicit: boolean }>(); private readonly _onDidSelectExpression = new Emitter(); private readonly _onWillUpdateViews = new Emitter(); - private multiSessionView: boolean; private expressionSelectedContextKey!: IContextKey; - private breakpointSelectedContextKey!: IContextKey; private loadedScriptsSupportedContextKey!: IContextKey; private stepBackSupportedContextKey!: IContextKey; private focusedSessionIsAttach!: IContextKey; @@ -31,12 +28,11 @@ export class ViewModel implements IViewModel { private stepIntoTargetsSupported!: IContextKey; private jumpToCursorSupported!: IContextKey; private setVariableSupported!: IContextKey; + private multiSessionDebug!: IContextKey; constructor(private contextKeyService: IContextKeyService) { - this.multiSessionView = false; contextKeyService.bufferChangeEvents(() => { this.expressionSelectedContextKey = CONTEXT_EXPRESSION_SELECTED.bindTo(contextKeyService); - this.breakpointSelectedContextKey = CONTEXT_BREAKPOINT_SELECTED.bindTo(contextKeyService); this.loadedScriptsSupportedContextKey = CONTEXT_LOADED_SCRIPTS_SUPPORTED.bindTo(contextKeyService); this.stepBackSupportedContextKey = CONTEXT_STEP_BACK_SUPPORTED.bindTo(contextKeyService); this.focusedSessionIsAttach = CONTEXT_FOCUSED_SESSION_IS_ATTACH.bindTo(contextKeyService); @@ -44,6 +40,7 @@ export class ViewModel implements IViewModel { this.stepIntoTargetsSupported = CONTEXT_STEP_INTO_TARGETS_SUPPORTED.bindTo(contextKeyService); this.jumpToCursorSupported = CONTEXT_JUMP_TO_CURSOR_SUPPORTED.bindTo(contextKeyService); this.setVariableSupported = CONTEXT_SET_VARIABLE_SUPPORTED.bindTo(contextKeyService); + this.multiSessionDebug = CONTEXT_MULTI_SESSION_DEBUG.bindTo(contextKeyService); }); } @@ -112,10 +109,6 @@ export class ViewModel implements IViewModel { return this._onDidSelectExpression.event; } - getSelectedBreakpoint(): IFunctionBreakpoint | IExceptionBreakpoint | undefined { - return this.selectedBreakpoint; - } - updateViews(): void { this._onWillUpdateViews.fire(); } @@ -124,16 +117,11 @@ export class ViewModel implements IViewModel { return this._onWillUpdateViews.event; } - setSelectedBreakpoint(breakpoint: IFunctionBreakpoint | IExceptionBreakpoint | undefined): void { - this.selectedBreakpoint = breakpoint; - this.breakpointSelectedContextKey.set(!!breakpoint); - } - isMultiSessionView(): boolean { - return this.multiSessionView; + return !!this.multiSessionDebug.get(); } setMultiSessionView(isMultiSessionView: boolean): void { - this.multiSessionView = isMultiSessionView; + this.multiSessionDebug.set(isMultiSessionView); } } diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index f10608a01..c46e6b9ca 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -15,6 +15,7 @@ import { Emitter, Event } from 'vs/base/common/event'; const MAX_REPL_LENGTH = 10000; let topReplElementCounter = 0; +const getUniqueId = () => `topReplElement:${topReplElementCounter++}`; export class SimpleReplElement implements IReplElement { @@ -30,8 +31,12 @@ export class SimpleReplElement implements IReplElement { ) { } toString(): string { + let valueRespectCount = this.value; + for (let i = 1; i < this.count; i++) { + valueRespectCount += (valueRespectCount.endsWith('\n') ? '' : '\n') + this.value; + } const sourceStr = this.sourceData ? ` ${this.sourceData.source.name}` : ''; - return this.value + sourceStr; + return valueRespectCount + sourceStr; } getId(): string { @@ -226,13 +231,14 @@ export class ReplModel { return; } if (!previousElement.value.endsWith('\n') && !previousElement.value.endsWith('\r\n') && previousElement.count === 1) { - previousElement.value += data; + this.replElements[this.replElements.length - 1] = new SimpleReplElement( + session, getUniqueId(), previousElement.value + data, sev, source); this._onDidChangeElements.fire(); return; } } - const element = new SimpleReplElement(session, `topReplElement:${topReplElementCounter++}`, data, sev, source); + const element = new SimpleReplElement(session, getUniqueId(), data, sev, source); this.addReplElement(element); } else { // TODO@Isidor hack, we should introduce a new type which is an output that can fetch children like an expression @@ -307,7 +313,7 @@ export class ReplModel { } // show object - this.appendToRepl(session, new RawObjectReplElement(`topReplElement:${topReplElementCounter++}`, (a).prototype, a, undefined, nls.localize('snapshotObj', "Only primitive values are shown for this object.")), sev, source); + this.appendToRepl(session, new RawObjectReplElement(getUniqueId(), (a).prototype, a, undefined, nls.localize('snapshotObj', "Only primitive values are shown for this object.")), sev, source); } // string: watch out for % replacement directive diff --git a/src/vs/workbench/contrib/debug/node/debugHelperService.ts b/src/vs/workbench/contrib/debug/node/debugHelperService.ts index a79291240..05be0f7d8 100644 --- a/src/vs/workbench/contrib/debug/node/debugHelperService.ts +++ b/src/vs/workbench/contrib/debug/node/debugHelperService.ts @@ -5,7 +5,7 @@ import { IDebugHelperService } from 'vs/workbench/contrib/debug/common/debug'; import { Client as TelemetryClient } from 'vs/base/parts/ipc/node/ipc.cp'; -import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; +import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; import { FileAccess } from 'vs/base/common/network'; import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -31,8 +31,8 @@ export class NodeDebugHelperService implements IDebugHelperService { args: args, env: { ELECTRON_RUN_AS_NODE: 1, - PIPE_LOGGING: 'true', - AMD_ENTRYPOINT: 'vs/workbench/contrib/debug/node/telemetryApp' + VSCODE_PIPE_LOGGING: 'true', + VSCODE_AMD_ENTRYPOINT: 'vs/workbench/contrib/debug/node/telemetryApp' } } ); diff --git a/src/vs/workbench/contrib/debug/node/telemetryApp.ts b/src/vs/workbench/contrib/debug/node/telemetryApp.ts index 52507e4fd..85597832c 100644 --- a/src/vs/workbench/contrib/debug/node/telemetryApp.ts +++ b/src/vs/workbench/contrib/debug/node/telemetryApp.ts @@ -5,7 +5,7 @@ import { Server } from 'vs/base/parts/ipc/node/ipc.cp'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; -import { TelemetryAppenderChannel } from 'vs/platform/telemetry/node/telemetryIpc'; +import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc'; const appender = new AppInsightsAppender(process.argv[2], JSON.parse(process.argv[3]), process.argv[4]); process.once('exit', () => appender.flush()); diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index 45a8fc654..03c4fc8f6 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -9,7 +9,7 @@ import { WindowsExternalTerminalService, MacExternalTerminalService, LinuxExtern import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/common/externalTerminal'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; -import { extractDriveLetter } from 'vs/base/common/labels'; +import { getDriveLetter } from 'vs/base/common/extpath'; let externalTerminalService: IExternalTerminalService | undefined = undefined; @@ -112,7 +112,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? }; if (cwd) { - const driveLetter = extractDriveLetter(cwd); + const driveLetter = getDriveLetter(cwd); if (driveLetter) { command += `${driveLetter}:; `; } @@ -145,7 +145,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? }; if (cwd) { - const driveLetter = extractDriveLetter(cwd); + const driveLetter = getDriveLetter(cwd); if (driveLetter) { command += `${driveLetter}: && `; } diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index 1a2bed974..fec15f39f 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -153,8 +153,8 @@ suite('Debug - Breakpoints', () => { test('function breakpoints', () => { model.addFunctionBreakpoint('foo', '1'); model.addFunctionBreakpoint('bar', '2'); - model.renameFunctionBreakpoint('1', 'fooUpdated'); - model.renameFunctionBreakpoint('2', 'barUpdated'); + model.updateFunctionBreakpoint('1', { name: 'fooUpdated' }); + model.updateFunctionBreakpoint('2', { name: 'barUpdated' }); const functionBps = model.getFunctionBreakpoints(); assert.equal(functionBps[0].name, 'fooUpdated'); diff --git a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts index ca7c0db50..dd6943746 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -20,6 +20,14 @@ import { generateUuid } from 'vs/base/common/uuid'; import { debugStackframe, debugStackframeFocused } from 'vs/workbench/contrib/debug/browser/debugIcons'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +const mockWorkspaceContextService = { + getWorkspace: () => { + return { + folders: [] + }; + } +} as any; + export function createMockSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession { return new DebugSession(generateUuid(), { resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, { getViewModel(): any { @@ -29,7 +37,7 @@ export function createMockSession(model: DebugModel, name = 'mockSession', optio } }; } - } as IDebugService, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); + } as IDebugService, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); } function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFrame, secondStackFrame: StackFrame } { @@ -91,7 +99,7 @@ suite('Debug - CallStack', () => { assert.equal(model.getSessions(true).length, 1); }); - test('threads multiple wtih allThreadsStopped', () => { + test('threads multiple wtih allThreadsStopped', async () => { const threadId1 = 1; const threadName1 = 'firstThread'; const threadId2 = 2; @@ -145,19 +153,16 @@ suite('Debug - CallStack', () => { // after calling getCallStack, the callstack becomes available // and results in a request for the callstack in the debug adapter - thread1.fetchCallStack().then(() => { - assert.notEqual(thread1.getCallStack().length, 0); - }); + await thread1.fetchCallStack(); + assert.notEqual(thread1.getCallStack().length, 0); - thread2.fetchCallStack().then(() => { - assert.notEqual(thread2.getCallStack().length, 0); - }); + await thread2.fetchCallStack(); + assert.notEqual(thread2.getCallStack().length, 0); // calling multiple times getCallStack doesn't result in multiple calls // to the debug adapter - thread1.fetchCallStack().then(() => { - return thread2.fetchCallStack(); - }); + await thread1.fetchCallStack(); + await thread2.fetchCallStack(); // clearing the callstack results in the callstack not being available thread1.clearCallStack(); @@ -174,7 +179,7 @@ suite('Debug - CallStack', () => { assert.equal(session.getAllThreads().length, 0); }); - test('threads mutltiple without allThreadsStopped', () => { + test('threads mutltiple without allThreadsStopped', async () => { const sessionStub = sinon.spy(rawSession, 'stackTrace'); const stoppedThreadId = 1; @@ -230,19 +235,17 @@ suite('Debug - CallStack', () => { // after calling getCallStack, the callstack becomes available // and results in a request for the callstack in the debug adapter - stoppedThread.fetchCallStack().then(() => { - assert.notEqual(stoppedThread.getCallStack().length, 0); - assert.equal(runningThread.getCallStack().length, 0); - assert.equal(sessionStub.callCount, 1); - }); + await stoppedThread.fetchCallStack(); + assert.notEqual(stoppedThread.getCallStack().length, 0); + assert.equal(runningThread.getCallStack().length, 0); + assert.equal(sessionStub.callCount, 1); // calling getCallStack on the running thread returns empty array // and does not return in a request for the callstack in the debug // adapter - runningThread.fetchCallStack().then(() => { - assert.equal(runningThread.getCallStack().length, 0); - assert.equal(sessionStub.callCount, 1); - }); + await runningThread.fetchCallStack(); + assert.equal(runningThread.getCallStack().length, 0); + assert.equal(sessionStub.callCount, 1); // clearing the callstack results in the callstack not being available stoppedThread.clearCallStack(); @@ -374,7 +377,7 @@ suite('Debug - CallStack', () => { get state(): State { return State.Stopped; } - }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); + }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); const runningSession = createMockSession(model); model.addSession(runningSession); diff --git a/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts similarity index 100% rename from src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts rename to src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts diff --git a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts index b22d2044a..552642346 100644 --- a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts @@ -64,7 +64,7 @@ suite('Debug - Link Detector', () => { test('singleLineLink', () => { const input = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; - const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34<\/a><\/span>' : '/Users/foo/bar.js:12:34<\/a><\/span>'; + const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34<\/a><\/span>' : '/Users/foo/bar.js:12:34<\/a><\/span>'; const output = linkDetector.linkify(input); assert.equal(1, output.children.length); @@ -87,7 +87,7 @@ suite('Debug - Link Detector', () => { test('relativeLinkWithWorkspace', () => { const input = '\./foo/bar.js'; - const expectedOutput = /^\.\/foo\/bar\.js<\/a><\/span>$/; + const expectedOutput = /^\.\/foo\/bar\.js<\/a><\/span>$/; const output = linkDetector.linkify(input, false, new WorkspaceFolder({ uri: URI.file('/path/to/workspace'), name: 'ws', index: 0 })); assert.equal('SPAN', output.tagName); @@ -96,7 +96,7 @@ suite('Debug - Link Detector', () => { test('singleLineLinkAndText', function () { const input = isWindows ? 'The link: C:/foo/bar.js:12:34' : 'The link: /Users/foo/bar.js:12:34'; - const expectedOutput = /^The link: .*\/foo\/bar.js:12:34<\/a><\/span>$/; + const expectedOutput = /^The link: .*\/foo\/bar.js:12:34<\/a><\/span>$/; const output = linkDetector.linkify(input); assert.equal(1, output.children.length); @@ -110,7 +110,7 @@ suite('Debug - Link Detector', () => { test('singleLineMultipleLinks', () => { const input = isWindows ? 'Here is a link C:/foo/bar.js:12:34 and here is another D:/boo/far.js:56:78' : 'Here is a link /Users/foo/bar.js:12:34 and here is another /Users/boo/far.js:56:78'; - const expectedOutput = /^Here is a link .*\/foo\/bar.js:12:34<\/a> and here is another .*\/boo\/far.js:56:78<\/a><\/span>$/; + const expectedOutput = /^Here is a link .*\/foo\/bar.js:12:34<\/a> and here is another .*\/boo\/far.js:56:78<\/a><\/span>$/; const output = linkDetector.linkify(input); assert.equal(2, output.children.length); @@ -152,7 +152,7 @@ suite('Debug - Link Detector', () => { test('multilineWithLinks', () => { const input = isWindows ? 'I have a link for you\nHere it is: C:/foo/bar.js:12:34\nCool, huh?' : 'I have a link for you\nHere it is: /Users/foo/bar.js:12:34\nCool, huh?'; - const expectedOutput = /^I have a link for you\n<\/span>Here it is: .*\/foo\/bar.js:12:34<\/a>\n<\/span>Cool, huh\?<\/span><\/span>$/; + const expectedOutput = /^I have a link for you\n<\/span>Here it is: .*\/foo\/bar.js:12:34<\/a>\n<\/span>Cool, huh\?<\/span><\/span>$/; const output = linkDetector.linkify(input, true); assert.equal(3, output.children.length); diff --git a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts index 96a346ab9..8983f96e3 100644 --- a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts @@ -24,29 +24,29 @@ export const mockUriIdentityService = new UriIdentityService(fileService); export class MockDebugService implements IDebugService { - public _serviceBrand: undefined; + _serviceBrand: undefined; - public get state(): State { + get state(): State { throw new Error('not implemented'); } - public get onWillNewSession(): Event { + get onWillNewSession(): Event { throw new Error('not implemented'); } - public get onDidNewSession(): Event { + get onDidNewSession(): Event { throw new Error('not implemented'); } - public get onDidEndSession(): Event { + get onDidEndSession(): Event { throw new Error('not implemented'); } - public get onDidChangeState(): Event { + get onDidChangeState(): Event { throw new Error('not implemented'); } - public getConfigurationManager(): IConfigurationManager { + getConfigurationManager(): IConfigurationManager { throw new Error('not implemented'); } @@ -58,7 +58,7 @@ export class MockDebugService implements IDebugService { throw new Error('Method not implemented.'); } - public focusStackFrame(focusedStackFrame: IStackFrame): Promise { + focusStackFrame(focusedStackFrame: IStackFrame): Promise { throw new Error('not implemented'); } @@ -66,23 +66,23 @@ export class MockDebugService implements IDebugService { throw new Error('not implemented'); } - public addBreakpoints(uri: uri, rawBreakpoints: IBreakpointData[]): Promise { + addBreakpoints(uri: uri, rawBreakpoints: IBreakpointData[]): Promise { throw new Error('not implemented'); } - public updateBreakpoints(uri: uri, data: Map, sendOnResourceSaved: boolean): Promise { + updateBreakpoints(uri: uri, data: Map, sendOnResourceSaved: boolean): Promise { throw new Error('not implemented'); } - public enableOrDisableBreakpoints(enabled: boolean): Promise { + enableOrDisableBreakpoints(enabled: boolean): Promise { throw new Error('not implemented'); } - public setBreakpointsActivated(): Promise { + setBreakpointsActivated(): Promise { throw new Error('not implemented'); } - public removeBreakpoints(): Promise { + removeBreakpoints(): Promise { throw new Error('not implemented'); } @@ -90,15 +90,19 @@ export class MockDebugService implements IDebugService { throw new Error('Method not implemented.'); } - public addFunctionBreakpoint(): void { } + setExceptionBreakpoints(data: DebugProtocol.ExceptionBreakpointsFilter[]): void { + throw new Error('Method not implemented.'); + } - public moveWatchExpression(id: string, position: number): void { } + addFunctionBreakpoint(): void { } - public renameFunctionBreakpoint(id: string, newFunctionName: string): Promise { + moveWatchExpression(id: string, position: number): void { } + + updateFunctionBreakpoint(id: string, update: { name?: string, hitCondition?: string, condition?: string }): Promise { throw new Error('not implemented'); } - public removeFunctionBreakpoints(id?: string): Promise { + removeFunctionBreakpoints(id?: string): Promise { throw new Error('not implemented'); } @@ -109,47 +113,47 @@ export class MockDebugService implements IDebugService { throw new Error('Method not implemented.'); } - public addReplExpression(name: string): Promise { + addReplExpression(name: string): Promise { throw new Error('not implemented'); } - public removeReplExpressions(): void { } + removeReplExpressions(): void { } - public addWatchExpression(name?: string): Promise { + addWatchExpression(name?: string): Promise { throw new Error('not implemented'); } - public renameWatchExpression(id: string, newName: string): Promise { + renameWatchExpression(id: string, newName: string): Promise { throw new Error('not implemented'); } - public removeWatchExpressions(id?: string): void { } + removeWatchExpressions(id?: string): void { } - public startDebugging(launch: ILaunch, configOrName?: IConfig | string, options?: IDebugSessionOptions): Promise { + startDebugging(launch: ILaunch, configOrName?: IConfig | string, options?: IDebugSessionOptions): Promise { return Promise.resolve(true); } - public restartSession(): Promise { + restartSession(): Promise { throw new Error('not implemented'); } - public stopSession(): Promise { + stopSession(): Promise { throw new Error('not implemented'); } - public getModel(): IDebugModel { + getModel(): IDebugModel { throw new Error('not implemented'); } - public getViewModel(): IViewModel { + getViewModel(): IViewModel { throw new Error('not implemented'); } - public logToRepl(session: IDebugSession, value: string): void { } + logToRepl(session: IDebugSession, value: string): void { } - public sourceIsNotAvailable(uri: uri): void { } + sourceIsNotAvailable(uri: uri): void { } - public tryToAutoFocusStackFrame(thread: IThread): Promise { + tryToAutoFocusStackFrame(thread: IThread): Promise { throw new Error('not implemented'); } } @@ -227,6 +231,10 @@ export class MockSession implements IDebugSession { return 'mockname'; } + get name(): string { + return 'mockname'; + } + setName(name: string): void { throw new Error('not implemented'); } @@ -387,14 +395,14 @@ export class MockRawSession { disconnected = false; sessionLengthInSeconds: number = 0; - public readyForBreakpoints = true; - public emittedStopped = true; + readyForBreakpoints = true; + emittedStopped = true; - public getLengthInSeconds(): number { + getLengthInSeconds(): number { return 100; } - public stackTrace(args: DebugProtocol.StackTraceArguments): Promise { + stackTrace(args: DebugProtocol.StackTraceArguments): Promise { return Promise.resolve({ seq: 1, type: 'response', @@ -412,19 +420,19 @@ export class MockRawSession { }); } - public exceptionInfo(args: DebugProtocol.ExceptionInfoArguments): Promise { + exceptionInfo(args: DebugProtocol.ExceptionInfoArguments): Promise { throw new Error('not implemented'); } - public launchOrAttach(args: IConfig): Promise { + launchOrAttach(args: IConfig): Promise { throw new Error('not implemented'); } - public scopes(args: DebugProtocol.ScopesArguments): Promise { + scopes(args: DebugProtocol.ScopesArguments): Promise { throw new Error('not implemented'); } - public variables(args: DebugProtocol.VariablesArguments): Promise { + variables(args: DebugProtocol.VariablesArguments): Promise { throw new Error('not implemented'); } @@ -432,87 +440,87 @@ export class MockRawSession { return Promise.resolve(null!); } - public custom(request: string, args: any): Promise { + custom(request: string, args: any): Promise { throw new Error('not implemented'); } - public terminate(restart = false): Promise { + terminate(restart = false): Promise { throw new Error('not implemented'); } - public disconnect(restart?: boolean): Promise { + disconnect(restart?: boolean): Promise { throw new Error('not implemented'); } - public threads(): Promise { + threads(): Promise { throw new Error('not implemented'); } - public stepIn(args: DebugProtocol.StepInArguments): Promise { + stepIn(args: DebugProtocol.StepInArguments): Promise { throw new Error('not implemented'); } - public stepOut(args: DebugProtocol.StepOutArguments): Promise { + stepOut(args: DebugProtocol.StepOutArguments): Promise { throw new Error('not implemented'); } - public stepBack(args: DebugProtocol.StepBackArguments): Promise { + stepBack(args: DebugProtocol.StepBackArguments): Promise { throw new Error('not implemented'); } - public continue(args: DebugProtocol.ContinueArguments): Promise { + continue(args: DebugProtocol.ContinueArguments): Promise { throw new Error('not implemented'); } - public reverseContinue(args: DebugProtocol.ReverseContinueArguments): Promise { + reverseContinue(args: DebugProtocol.ReverseContinueArguments): Promise { throw new Error('not implemented'); } - public pause(args: DebugProtocol.PauseArguments): Promise { + pause(args: DebugProtocol.PauseArguments): Promise { throw new Error('not implemented'); } - public terminateThreads(args: DebugProtocol.TerminateThreadsArguments): Promise { + terminateThreads(args: DebugProtocol.TerminateThreadsArguments): Promise { throw new Error('not implemented'); } - public setVariable(args: DebugProtocol.SetVariableArguments): Promise { + setVariable(args: DebugProtocol.SetVariableArguments): Promise { throw new Error('not implemented'); } - public restartFrame(args: DebugProtocol.RestartFrameArguments): Promise { + restartFrame(args: DebugProtocol.RestartFrameArguments): Promise { throw new Error('not implemented'); } - public completions(args: DebugProtocol.CompletionsArguments): Promise { + completions(args: DebugProtocol.CompletionsArguments): Promise { throw new Error('not implemented'); } - public next(args: DebugProtocol.NextArguments): Promise { + next(args: DebugProtocol.NextArguments): Promise { throw new Error('not implemented'); } - public source(args: DebugProtocol.SourceArguments): Promise { + source(args: DebugProtocol.SourceArguments): Promise { throw new Error('not implemented'); } - public loadedSources(args: DebugProtocol.LoadedSourcesArguments): Promise { + loadedSources(args: DebugProtocol.LoadedSourcesArguments): Promise { throw new Error('not implemented'); } - public setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): Promise { + setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): Promise { throw new Error('not implemented'); } - public setFunctionBreakpoints(args: DebugProtocol.SetFunctionBreakpointsArguments): Promise { + setFunctionBreakpoints(args: DebugProtocol.SetFunctionBreakpointsArguments): Promise { throw new Error('not implemented'); } - public setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): Promise { + setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): Promise { throw new Error('not implemented'); } - public readonly onDidStop: Event = null!; + readonly onDidStop: Event = null!; } export class MockDebugAdapter extends AbstractDebugAdapter { diff --git a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts index 288fa962a..e8826837f 100644 --- a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts @@ -74,11 +74,11 @@ suite('Debug - REPL', () => { repl.appendToRepl(session, 'third line', severity.Info); elements = repl.getReplElements(); assert.equal(elements.length, 3); - assert.equal(elements[0], 'first line\n'); + assert.equal(elements[0].value, 'first line\n'); assert.equal(elements[0].count, 3); - assert.equal(elements[1], 'second line'); + assert.equal(elements[1].value, 'second line'); assert.equal(elements[1].count, 2); - assert.equal(elements[2], 'third line'); + assert.equal(elements[2].value, 'third line'); assert.equal(elements[2].count, 1); }); @@ -93,11 +93,13 @@ suite('Debug - REPL', () => { repl.appendToRepl(session, 'third line', severity.Info); const elements = repl.getReplElements(); assert.equal(elements.length, 3); - assert.equal(elements[0], 'first line\n'); + assert.equal(elements[0].value, 'first line\n'); + assert.equal(elements[0].toString(), 'first line\nfirst line\nfirst line\n'); assert.equal(elements[0].count, 3); - assert.equal(elements[1], 'second line'); + assert.equal(elements[1].value, 'second line'); + assert.equal(elements[1].toString(), 'second line\nsecond line'); assert.equal(elements[1].count, 2); - assert.equal(elements[2], 'third line'); + assert.equal(elements[2].value, 'third line'); assert.equal(elements[2].count, 1); }); diff --git a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts index 68a34b4b1..fd61d857b 100644 --- a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts @@ -11,7 +11,7 @@ import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { URI } from 'vs/base/common/uri'; import { ExecutableDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; -import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/modelService.test'; +import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index 96c54f0a8..feae80004 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -27,7 +27,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ILabelService } from 'vs/platform/label/common/label'; -import { renderCodicons } from 'vs/base/browser/codicons'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { Schemas } from 'vs/base/common/network'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -297,21 +297,28 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { const activationId = activationTimes.activationReason.extensionId.value; const activationEvent = activationTimes.activationReason.activationEvent; if (activationEvent === '*') { - title = nls.localize('starActivation', "Activated by {0} on start-up", activationId); + title = nls.localize({ + key: 'starActivation', + comment: [ + '{0} will be an extension identifier' + ] + }, "Activated by {0} on start-up", activationId); } else if (/^workspaceContains:/.test(activationEvent)) { let fileNameOrGlob = activationEvent.substr('workspaceContains:'.length); if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0) { title = nls.localize({ key: 'workspaceContainsGlobActivation', comment: [ - '{0} will be a glob pattern' + '{0} will be a glob pattern', + '{1} will be an extension identifier' ] - }, "Activated by {1} because a file matching {1} exists in your workspace", fileNameOrGlob, activationId); + }, "Activated by {1} because a file matching {0} exists in your workspace", fileNameOrGlob, activationId); } else { title = nls.localize({ key: 'workspaceContainsFileActivation', comment: [ - '{0} will be a file name' + '{0} will be a file name', + '{1} will be an extension identifier' ] }, "Activated by {1} because file {0} exists in your workspace", fileNameOrGlob, activationId); } @@ -320,7 +327,8 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { title = nls.localize({ key: 'workspaceContainsTimeout', comment: [ - '{0} will be a glob pattern' + '{0} will be a glob pattern', + '{1} will be an extension identifier' ] }, "Activated by {1} because searching for {0} took too long", glob, activationId); } else if (activationEvent === 'onStartupFinished') { @@ -337,7 +345,8 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { title = nls.localize({ key: 'workspaceGenericActivation', comment: [ - 'The {0} placeholder will be an activation event, like e.g. \'language:typescript\', \'debug\', etc.' + '{0} will be an activation event, like e.g. \'language:typescript\', \'debug\', etc.', + '{1} will be an extension identifier' ] }, "Activated by {1} on {0}", activationEvent, activationId); } @@ -346,28 +355,28 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { clearNode(data.msgContainer); if (this._getUnresponsiveProfile(element.description.identifier)) { - const el = $('span', undefined, ...renderCodicons(` $(alert) Unresponsive`)); + const el = $('span', undefined, ...renderLabelWithIcons(` $(alert) Unresponsive`)); el.title = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze."); data.msgContainer.appendChild(el); } if (isNonEmptyArray(element.status.runtimeErrors)) { - const el = $('span', undefined, ...renderCodicons(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`)); + const el = $('span', undefined, ...renderLabelWithIcons(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`)); data.msgContainer.appendChild(el); } if (element.status.messages && element.status.messages.length > 0) { - const el = $('span', undefined, ...renderCodicons(`$(alert) ${element.status.messages[0].message}`)); + const el = $('span', undefined, ...renderLabelWithIcons(`$(alert) ${element.status.messages[0].message}`)); data.msgContainer.appendChild(el); } if (element.description.extensionLocation.scheme === Schemas.vscodeRemote) { - const el = $('span', undefined, ...renderCodicons(`$(remote) ${element.description.extensionLocation.authority}`)); + const el = $('span', undefined, ...renderLabelWithIcons(`$(remote) ${element.description.extensionLocation.authority}`)); data.msgContainer.appendChild(el); const hostLabel = this._labelService.getHostLabel(Schemas.vscodeRemote, this._environmentService.remoteAuthority); if (hostLabel) { - reset(el, ...renderCodicons(`$(remote) ${hostLabel}`)); + reset(el, ...renderLabelWithIcons(`$(remote) ${hostLabel}`)); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 7a58b96da..e7f0a15b3 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -65,22 +65,14 @@ import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/to import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { insane } from 'vs/base/common/insane/insane'; function removeEmbeddedSVGs(documentContent: string): string { - const newDocument = new DOMParser().parseFromString(documentContent, 'text/html'); - - // remove all inline svgs - const allSVGs = newDocument.documentElement.querySelectorAll('svg'); - if (allSVGs) { - for (let i = 0; i < allSVGs.length; i++) { - const svg = allSVGs[i]; - if (svg.parentNode) { - svg.parentNode.removeChild(allSVGs[i]); - } + return insane(documentContent, { + filter(token: { tag: string, attrs: { readonly [key: string]: string } }): boolean { + return token.tag !== 'svg'; } - } - - return newDocument.documentElement.outerHTML; + }); } class NavBar extends Disposable { @@ -169,6 +161,11 @@ interface IExtensionEditorTemplate { header: HTMLElement; } +const enum WebviewIndex { + Readme, + Changelog +} + export class ExtensionEditor extends EditorPane { static readonly ID: string = 'workbench.editor.extension'; @@ -179,6 +176,12 @@ export class ExtensionEditor extends EditorPane { private extensionChangelog: Cache | null; private extensionManifest: Cache | null; + // Some action bar items use a webview whose vertical scroll position we track in this map + private initialScrollProgress: Map = new Map(); + + // Spot when an ExtensionEditor instance gets reused for a different extension, in which case the vertical scroll positions must be zeroed + private currentIdentifier: string = ''; + private layoutParticipants: ILayoutParticipant[] = []; private readonly contentDisposables = this._register(new DisposableStore()); private readonly transientDisposables = this._register(new DisposableStore()); @@ -336,6 +339,11 @@ export class ExtensionEditor extends EditorPane { this.editorLoadComplete = false; const extension = input.extension; + if (this.currentIdentifier !== extension.identifier.id) { + this.initialScrollProgress.clear(); + this.currentIdentifier = extension.identifier.id; + } + this.transientDisposables.clear(); this.extensionReadme = new Cache(() => createCancelablePromise(token => extension.getReadme(token))); @@ -563,7 +571,7 @@ export class ExtensionEditor extends EditorPane { return Promise.resolve(null); } - private async openMarkdown(cacheResult: CacheResult, noContentCopy: string, template: IExtensionEditorTemplate, token: CancellationToken): Promise { + private async openMarkdown(cacheResult: CacheResult, noContentCopy: string, template: IExtensionEditorTemplate, webviewIndex: WebviewIndex, token: CancellationToken): Promise { try { const body = await this.renderMarkdown(cacheResult, template); if (token.isCancellationRequested) { @@ -572,15 +580,22 @@ export class ExtensionEditor extends EditorPane { const webview = this.contentDisposables.add(this.webviewService.createWebviewOverlay('extensionEditor', { enableFindWidget: true, + tryRestoreScrollPosition: true, }, {}, undefined)); + webview.initialScrollProgress = this.initialScrollProgress.get(webviewIndex) || 0; + webview.claim(this, this.scopedContextKeyService); setParentFlowTo(webview.container, template.content); webview.layoutWebviewOverElement(template.content); webview.html = body; + webview.claim(this, undefined); this.contentDisposables.add(webview.onDidFocus(() => this.fireOnDidFocus())); + + this.contentDisposables.add(webview.onDidScroll(() => this.initialScrollProgress.set(webviewIndex, webview.initialScrollProgress))); + const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout: () => { webview.layoutWebviewOverElement(template.content); @@ -622,8 +637,8 @@ export class ExtensionEditor extends EditorPane { private async renderMarkdown(cacheResult: CacheResult, template: IExtensionEditorTemplate) { const contents = await this.loadContents(() => cacheResult, template); const content = await renderMarkdownDocument(contents, this.extensionService, this.modeService); - const documentContent = await this.renderBody(content); - return removeEmbeddedSVGs(documentContent); + const sanitizedContent = removeEmbeddedSVGs(content); + return await this.renderBody(sanitizedContent); } private async renderBody(body: string): Promise { @@ -709,9 +724,16 @@ export class ExtensionEditor extends EditorPane { } code { + font-family: "SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace; + font-size: 14px; + line-height: 19px; + } + + pre code { font-family: var(--vscode-editor-font-family); font-weight: var(--vscode-editor-font-weight); font-size: var(--vscode-editor-font-size); + line-height: 1.5; } code > div { @@ -823,7 +845,7 @@ export class ExtensionEditor extends EditorPane { if (manifest && manifest.extensionPack && manifest.extensionPack.length) { return this.openExtensionPackReadme(manifest, template, token); } - return this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), template, token); + return this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), template, WebviewIndex.Readme, token); } private async openExtensionPackReadme(manifest: IExtensionManifest, template: IExtensionEditorTemplate, token: CancellationToken): Promise { @@ -855,14 +877,14 @@ export class ExtensionEditor extends EditorPane { await Promise.all([ this.renderExtensionPack(manifest, extensionPackContent, token), - this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), { ...template, ...{ content: readmeContent } }, token), + this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), { ...template, ...{ content: readmeContent } }, WebviewIndex.Readme, token), ]); return { focus: () => extensionPackContent.focus() }; } private openChangelog(template: IExtensionEditorTemplate, token: CancellationToken): Promise { - return this.openMarkdown(this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), template, token); + return this.openMarkdown(this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), template, WebviewIndex.Changelog, token); } private openContributions(template: IExtensionEditorTemplate, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index ab902f413..d48a5cde2 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -13,7 +13,7 @@ import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; +import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource, RecommendationSourceToString } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { INotificationHandle, INotificationService, IPromptChoice, IPromptChoiceWithMenu, Severity } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -21,6 +21,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; import { EnablementState, IWorkbenchExtensioManagementService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; @@ -28,6 +29,7 @@ import { IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/e type ExtensionRecommendationsNotificationClassification = { userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; extensionId?: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; + source: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; type ExtensionWorkspaceRecommendationsNotificationClassification = { @@ -135,6 +137,7 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec @IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService, @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, @IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, ) { this.tasExperimentService = tasExperimentService; @@ -153,13 +156,13 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec } return this.promptRecommendationsNotification(extensionIds, message, searchValue, source, { - onDidInstallRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id })), - onDidShowRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id })), - onDidCancelRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id })), + onDidInstallRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string, source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id, source: RecommendationSourceToString(source) })), + onDidShowRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string, source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id, source: RecommendationSourceToString(source) })), + onDidCancelRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string, source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id, source: RecommendationSourceToString(source) })), onDidNeverShowRecommendedExtensionsAgain: (extensions: IExtension[]) => { for (const extension of extensions) { this.addToImportantRecommendationsIgnore(extension.identifier.id); - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id }); + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string, source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id, source: RecommendationSourceToString(source) }); } this.notificationService.prompt( Severity.Info, @@ -207,6 +210,11 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec return RecommendationsNotificationResult.Ignored; } + // Do not show exe based recommendations in remote window + if (source === RecommendationSource.EXE && this.workbenchEnvironmentService.remoteAuthority) { + return RecommendationsNotificationResult.IncompatibleWindow; + } + // Ignore exe recommendation if the window // => has shown an exe based recommendation already // => or has shown any two recommendations already diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 839c3ffbe..4cb6024b2 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -6,20 +6,16 @@ import { localize } from 'vs/nls'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; -import { MenuRegistry, MenuId, registerAction2, Action2, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, registerAction2, Action2, SyncActionDescriptor, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ExtensionsLabel, ExtensionsLocalizedLabel, ExtensionsChannelId, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { ExtensionsLabel, ExtensionsLocalizedLabel, ExtensionsChannelId, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; -import { - OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction, - ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, ShowBuiltInExtensionsAction, UpdateAllAction, - EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, RefreshExtensionsAction -} from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, DefaultViewsContext, ExtensionsSortByContext, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/browser/extensionsViewlet'; @@ -37,11 +33,10 @@ import { ExtensionActivationProgress } from 'vs/workbench/contrib/extensions/bro import { onUnexpectedError } from 'vs/base/common/errors'; import { ExtensionDependencyChecker } from 'vs/workbench/contrib/extensions/browser/extensionsDependencyChecker'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { RemoteExtensionsInstaller } from 'vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller'; -import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; +import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions, IViewsService } from 'vs/workbench/common/views'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -import { ContextKeyAndExpr, ContextKeyExpr, ContextKeyOrExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyAndExpr, ContextKeyDefinedExpr, ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyOrExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; import { InstallExtensionQuickAccessProvider, ManageExtensionsQuickAccessProvider } from 'vs/workbench/contrib/extensions/browser/extensionsQuickAccess'; @@ -65,7 +60,15 @@ import { IAction } from 'vs/base/common/actions'; import { IWorkpsaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; import { Schemas } from 'vs/base/common/network'; import { ShowRuntimeExtensionsAction } from 'vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor'; -import { extensionsViewIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { clearSearchResultsIcon, configureRecommendedIcon, extensionsViewIcon, filterIcon, installWorkspaceRecommendedIcon, refreshIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { isArray } from 'vs/base/common/types'; +import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -83,16 +86,6 @@ Registry.as(Extensions.Quickaccess).registerQuickAccessPro helpEntries: [{ description: localize('manageExtensionsHelp', "Manage Extensions"), needsEditor: false }] }); -// Explorer -MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { - group: 'extensions', - command: { - id: INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, - title: localize('installVSIX', "Install Extension VSIX"), - }, - when: ResourceContextKey.Extension.isEqualTo('.vsix') -}); - // Editor Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( @@ -226,36 +219,6 @@ CommandsRegistry.registerCommand({ } }); -CommandsRegistry.registerCommand({ - id: INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, - handler: async (accessor: ServicesAccessor, resources: URI[] | URI) => { - const extensionService = accessor.get(IExtensionService); - const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); - const hostService = accessor.get(IHostService); - const notificationService = accessor.get(INotificationService); - - const extensions = Array.isArray(resources) ? resources : [resources]; - await Promise.all(extensions.map(async (vsix) => await extensionsWorkbenchService.install(vsix))) - .then(async (extensions) => { - for (const extension of extensions) { - const requireReload = !(extension.local && extensionService.canAddExtension(toExtensionDescription(extension.local))); - const message = requireReload ? localize('InstallVSIXAction.successReload', "Completed installing {0} extension from VSIX. Please reload Visual Studio Code to enable it.", extension.displayName || extension.name) - : localize('InstallVSIXAction.success', "Completed installing {0} extension from VSIX.", extension.displayName || extension.name); - const actions = requireReload ? [{ - label: localize('InstallVSIXAction.reloadNow', "Reload Now"), - run: () => hostService.reload() - }] : []; - notificationService.prompt( - Severity.Info, - message, - actions, - { sticky: true } - ); - } - }); - } -}); - CommandsRegistry.registerCommand({ id: 'workbench.extensions.uninstallExtension', description: { @@ -316,57 +279,6 @@ CommandsRegistry.registerCommand({ } }); -// File menu registration - -MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '2_keybindings', - command: { - id: ShowRecommendedKeymapExtensionsAction.ID, - title: localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymaps") - }, - order: 2 -}); - -MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '2_keybindings', - command: { - id: ShowRecommendedKeymapExtensionsAction.ID, - title: localize('miOpenKeymapExtensions2', "Keymaps") - }, - order: 2 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '1_settings', - command: { - id: VIEWLET_ID, - title: localize({ key: 'miPreferencesExtensions', comment: ['&& denotes a mnemonic'] }, "&&Extensions") - }, - order: 3 -}); - -// View menu - -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '3_views', - command: { - id: VIEWLET_ID, - title: localize({ key: 'miViewExtensions', comment: ['&& denotes a mnemonic'] }, "E&&xtensions") - }, - order: 5 -}); - -// Global Activity Menu - -MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '2_configuration', - command: { - id: VIEWLET_ID, - title: localize('showExtensions', "Extensions") - }, - order: 3 -}); - function overrideActionForActiveExtensionEditorWebview(command: MultiCommand | undefined, f: (webview: Webview) => void) { command?.addImplementation(105, (accessor) => { const editorService = accessor.get(IEditorService); @@ -399,13 +311,25 @@ async function runAction(action: IAction): Promise { } } -class ExtensionsContributions implements IWorkbenchContribution { +interface IExtensionActionOptions extends IAction2Options { + menuTitles?: { [id: number]: string }; + run(accessor: ServicesAccessor, ...args: any[]): Promise; +} + +class ExtensionsContributions extends Disposable implements IWorkbenchContribution { constructor( @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @IContextKeyService contextKeyService: IContextKeyService, + @IViewletService private readonly viewletService: IViewletService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @INotificationService private readonly notificationService: INotificationService, + @ICommandService private readonly commandService: ICommandService, ) { + super(); const hasGalleryContext = CONTEXT_HAS_GALLERY.bindTo(contextKeyService); if (extensionGalleryService.isEnabled()) { hasGalleryContext.set(true); @@ -447,437 +371,667 @@ class ExtensionsContributions implements IWorkbenchContribution { // Global actions private registerGlobalActions(): void { - registerAction2(class extends Action2 { - constructor() { - super({ - id: OpenExtensionsViewletAction.ID, - title: { value: OpenExtensionsViewletAction.LABEL, original: 'Show Extensions' }, - category: CATEGORIES.View, - menu: { - id: MenuId.CommandPalette, - }, - keybinding: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_X, - weight: KeybindingWeight.WorkbenchContrib + this.registerExtensionAction({ + id: VIEWLET_ID, + title: { value: localize('toggleExtensionsViewlet', "Show Extensions"), original: 'Show Extensions' }, + category: CATEGORIES.View, + menu: [{ + id: MenuId.CommandPalette, + }, { + id: MenuId.MenubarPreferencesMenu, + group: '1_settings', + order: 3 + }, { + id: MenuId.MenubarViewMenu, + group: '3_views', + order: 5 + }, { + id: MenuId.GlobalActivity, + group: '2_configuration', + order: 3 + }], + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_X, + weight: KeybindingWeight.WorkbenchContrib + }, + menuTitles: { + [MenuId.MenubarPreferencesMenu.id]: localize({ key: 'miPreferencesExtensions', comment: ['&& denotes a mnemonic'] }, "&&Extensions"), + [MenuId.MenubarViewMenu.id]: localize({ key: 'miViewExtensions', comment: ['&& denotes a mnemonic'] }, "E&&xtensions"), + [MenuId.GlobalActivity.id]: localize('showExtensions', "Extensions"), + }, + run: () => runAction(this.instantiationService.createInstance(ShowViewletAction, VIEWLET_ID, 'Show Extensions', VIEWLET_ID)) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.installExtensions', + title: { value: localize('installExtensions', "Install Extensions"), original: 'Install Extensions' }, + category: ExtensionsLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, + run: () => runAction(this.instantiationService.createInstance(ShowViewletAction, VIEWLET_ID, 'Install Extensions', VIEWLET_ID)) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showRecommendedKeymapExtensions', + title: { value: localize('showRecommendedKeymapExtensionsShort', "Keymaps"), original: 'Keymaps' }, + category: PreferencesLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: MenuId.MenubarPreferencesMenu, + group: '2_keybindings', + order: 2 + }, { + id: MenuId.GlobalActivity, + group: '2_keybindings', + order: 2 + }], + keybinding: { + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_M), + weight: KeybindingWeight.WorkbenchContrib + }, + menuTitles: { + [MenuId.MenubarPreferencesMenu.id]: localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymaps"), + [MenuId.GlobalActivity.id]: localize('miOpenKeymapExtensions2', "Keymaps") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recommended:keymaps ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showLanguageExtensions', + title: { value: localize('showLanguageExtensionsShort', "Language Extensions"), original: 'Language Extensions' }, + category: PreferencesLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@category:"programming languages" @sort:installs ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.checkForUpdates', + title: { value: localize('checkForUpdates', "Check for Extension Updates"), original: 'Check for Extension Updates' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: '1_updates', + order: 1 + }], + run: async () => { + await this.extensionsWorkbenchService.checkForUpdates(); + const outdated = this.extensionsWorkbenchService.outdated; + if (!outdated.length) { + this.notificationService.info(localize('noUpdatesAvailable', "All extensions are up to date.")); + return; + } + + let msgAvailableExtensions = outdated.length === 1 ? localize('singleUpdateAvailable', "An extension update is available.") : localize('updatesAvailable', "{0} extension updates are available.", outdated.length); + + const disabledExtensionsCount = outdated.filter(ext => ext.local && !this.extensionEnablementService.isEnabled(ext.local)).length; + if (disabledExtensionsCount) { + if (outdated.length === 1) { + msgAvailableExtensions = localize('singleDisabledUpdateAvailable', "An update to an extension which is disabled is available."); + } else if (disabledExtensionsCount === 1) { + msgAvailableExtensions = localize('updatesAvailableOneDisabled', "{0} extension updates are available. One of them is for a disabled extension.", outdated.length); + } else if (disabledExtensionsCount === outdated.length) { + msgAvailableExtensions = localize('updatesAvailableAllDisabled', "{0} extension updates are available. All of them are for disabled extensions.", outdated.length); + } else { + msgAvailableExtensions = localize('updatesAvailableIncludingDisabled', "{0} extension updates are available. {1} of them are for disabled extensions.", outdated.length, disabledExtensionsCount); } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(OpenExtensionsViewletAction, OpenExtensionsViewletAction.ID, OpenExtensionsViewletAction.LABEL)); + } + + this.viewletService.openViewlet(VIEWLET_ID, true); + this.notificationService.info(msgAvailableExtensions); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: InstallExtensionsAction.ID, - title: { value: InstallExtensionsAction.LABEL, original: 'Install Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + this.registerExtensionAction({ + id: 'workbench.extensions.action.disableAutoUpdate', + title: { value: localize('disableAutoUpdate', "Disable Auto Updating Extensions"), original: 'Disable Auto Updating Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), ContextKeyDefinedExpr.create(`config.${AutoUpdateConfigurationKey}`)]), + group: '1_updates', + order: 2 + }], + run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, false) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.updateAllExtensions', + title: { value: localize('updateAll', "Update All Extensions"), original: 'Update All Extensions' }, + category: ExtensionsLocalizedLabel, + precondition: HasOutdatedExtensionsContext, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), ContextKeyDefinedExpr.create(`config.${AutoUpdateConfigurationKey}`).negate()]), + group: '1_updates', + order: 2 + }], + run: () => { + return Promise.all(this.extensionsWorkbenchService.outdated.map(async extension => { + try { + await this.extensionsWorkbenchService.install(extension); + } catch (err) { + runAction(this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, err)); } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(InstallExtensionsAction, InstallExtensionsAction.ID, InstallExtensionsAction.LABEL)); + })); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowOutdatedExtensionsAction.ID, - title: { value: ShowOutdatedExtensionsAction.LABEL, original: 'Show Outdated Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, ShowOutdatedExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.enableAutoUpdate', + title: { value: localize('enableAutoUpdate', "Enable Auto Updating Extensions"), original: 'Enable Auto Updating Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), ContextKeyDefinedExpr.create(`config.${AutoUpdateConfigurationKey}`).negate()]), + group: '1_updates', + order: 3 + }], + run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, true) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.enableAll', + title: { value: localize('enableAll', "Enable All Extensions"), original: 'Enable All Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: '2_enablement', + order: 1 + }], + run: async () => { + const extensionsToEnable = this.extensionsWorkbenchService.local.filter(e => !!e.local && this.extensionEnablementService.canChangeEnablement(e.local) && !this.extensionEnablementService.isEnabled(e.local)); + if (extensionsToEnable.length) { + await this.extensionsWorkbenchService.setEnablement(extensionsToEnable, EnablementState.EnabledGlobally); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowRecommendedExtensionsAction.ID, - title: { value: ShowRecommendedExtensionsAction.LABEL, original: 'Show Recommended Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: CONTEXT_HAS_GALLERY - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.enableAllWorkspace', + title: { value: localize('enableAllWorkspace', "Enable All Extensions for this Workspace"), original: 'Enable All Extensions for this Workspace' }, + category: ExtensionsLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([WorkbenchStateContext.notEqualsTo('empty'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, + run: async () => { + const extensionsToEnable = this.extensionsWorkbenchService.local.filter(e => !!e.local && this.extensionEnablementService.canChangeEnablement(e.local) && !this.extensionEnablementService.isEnabled(e.local)); + if (extensionsToEnable.length) { + await this.extensionsWorkbenchService.setEnablement(extensionsToEnable, EnablementState.EnabledWorkspace); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowRecommendedKeymapExtensionsAction.ID, - title: { value: ShowRecommendedKeymapExtensionsAction.LABEL, original: 'Keymaps' }, - category: PreferencesLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: CONTEXT_HAS_GALLERY - }, - keybinding: { - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_M), - weight: KeybindingWeight.WorkbenchContrib - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowRecommendedKeymapExtensionsAction, ShowRecommendedKeymapExtensionsAction.ID, ShowRecommendedKeymapExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.disableAll', + title: { value: localize('disableAll', "Disable All Installed Extensions"), original: 'Disable All Installed Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: '2_enablement', + order: 2 + }], + run: async () => { + const extensionsToDisable = this.extensionsWorkbenchService.local.filter(e => !e.isBuiltin && !!e.local && this.extensionEnablementService.isEnabled(e.local) && this.extensionEnablementService.canChangeEnablement(e.local)); + if (extensionsToDisable.length) { + await this.extensionsWorkbenchService.setEnablement(extensionsToDisable, EnablementState.DisabledGlobally); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowLanguageExtensionsAction.ID, - title: { value: ShowLanguageExtensionsAction.LABEL, original: 'Language Extensions' }, - category: PreferencesLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: CONTEXT_HAS_GALLERY - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowLanguageExtensionsAction, ShowLanguageExtensionsAction.ID, ShowLanguageExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.disableAllWorkspace', + title: { value: localize('disableAllWorkspace', "Disable All Installed Extensions for this Workspace"), original: 'Disable All Installed Extensions for this Workspace' }, + category: ExtensionsLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([WorkbenchStateContext.notEqualsTo('empty'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, + run: async () => { + const extensionsToDisable = this.extensionsWorkbenchService.local.filter(e => !e.isBuiltin && !!e.local && this.extensionEnablementService.isEnabled(e.local) && this.extensionEnablementService.canChangeEnablement(e.local)); + if (extensionsToDisable.length) { + await this.extensionsWorkbenchService.setEnablement(extensionsToDisable, EnablementState.DisabledWorkspace); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowPopularExtensionsAction.ID, - title: { value: ShowPopularExtensionsAction.LABEL, original: 'Show Popular Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: CONTEXT_HAS_GALLERY - } + this.registerExtensionAction({ + id: SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, + title: { value: localize('InstallFromVSIX', "Install from VSIX..."), original: 'Install from VSIX...' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER])]), + group: '3_install', + order: 1 + }], + run: async (accessor: ServicesAccessor) => { + const fileDialogService = accessor.get(IFileDialogService); + const commandService = accessor.get(ICommandService); + const vsixPaths = await fileDialogService.showOpenDialog({ + title: localize('installFromVSIX', "Install from VSIX"), + filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }], + canSelectFiles: true, + canSelectMany: true, + openLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install")) }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowPopularExtensionsAction, ShowPopularExtensionsAction.ID, ShowPopularExtensionsAction.LABEL)); + if (vsixPaths) { + await commandService.executeCommand(INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, vsixPaths); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowEnabledExtensionsAction.ID, - title: { value: ShowEnabledExtensionsAction.LABEL, original: 'Show Enabled Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowEnabledExtensionsAction, ShowEnabledExtensionsAction.ID, ShowEnabledExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, + title: localize('installVSIX', "Install Extension VSIX"), + menu: [{ + id: MenuId.ExplorerContext, + group: 'extensions', + when: ContextKeyAndExpr.create([ResourceContextKey.Extension.isEqualTo('.vsix'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER])]), + }], + run: async (accessor: ServicesAccessor, resources: URI[] | URI) => { + const extensionService = accessor.get(IExtensionService); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const hostService = accessor.get(IHostService); + const notificationService = accessor.get(INotificationService); + + const extensions = Array.isArray(resources) ? resources : [resources]; + await Promise.all(extensions.map(async (vsix) => await extensionsWorkbenchService.install(vsix))) + .then(async (extensions) => { + for (const extension of extensions) { + const requireReload = !(extension.local && extensionService.canAddExtension(toExtensionDescription(extension.local))); + const message = requireReload ? localize('InstallVSIXAction.successReload', "Completed installing {0} extension from VSIX. Please reload Visual Studio Code to enable it.", extension.displayName || extension.name) + : localize('InstallVSIXAction.success', "Completed installing {0} extension from VSIX.", extension.displayName || extension.name); + const actions = requireReload ? [{ + label: localize('InstallVSIXAction.reloadNow', "Reload Now"), + run: () => hostService.reload() + }] : []; + notificationService.prompt( + Severity.Info, + message, + actions, + { sticky: true } + ); + } + }); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowInstalledExtensionsAction.ID, - title: { value: ShowInstalledExtensionsAction.LABEL, original: 'Show Installed Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL)); + const extensionsFilterSubMenu = new MenuId('extensionsFilterSubMenu'); + MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + submenu: extensionsFilterSubMenu, + title: localize('filterExtensions', "Filter Extensions..."), + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: 'navigation', + order: 1, + icon: filterIcon, + }); + + const showFeaturedExtensionsId = 'extensions.filter.featured'; + this.registerExtensionAction({ + id: showFeaturedExtensionsId, + title: { value: localize('showFeaturedExtensions', "Show Featured Extensions"), original: 'Show Featured Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: extensionsFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + group: '1_predefined', + order: 1, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('featured filter', "Featured") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@featured ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showPopularExtensions', + title: { value: localize('showPopularExtensions', "Show Popular Extensions"), original: 'Show Popular Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: extensionsFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + group: '1_predefined', + order: 2, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('most popular filter', "Most Popular") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@popular ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showRecommendedExtensions', + title: { value: localize('showRecommendedExtensions', "Show Recommended Extensions"), original: 'Show Recommended Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: extensionsFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + group: '1_predefined', + order: 2, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('most popular recommended', "Recommended") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recommended ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.recentlyPublishedExtensions', + title: { value: localize('recentlyPublishedExtensions', "Show Recently Published Extensions"), original: 'Show Recently Published Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: extensionsFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + group: '1_predefined', + order: 2, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('recently published filter', "Recently Published") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@sort:publishedDate ')) + }); + + const extensionsCategoryFilterSubMenu = new MenuId('extensionsCategoryFilterSubMenu'); + MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { + submenu: extensionsCategoryFilterSubMenu, + title: localize('filter by category', "Category"), + when: CONTEXT_HAS_GALLERY, + group: '2_categories', + order: 1, + }); + + EXTENSION_CATEGORIES.map((category, index) => { + this.registerExtensionAction({ + id: `extensions.actions.searchByCategory.${category}`, + title: category, + menu: [{ + id: extensionsCategoryFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + order: index, + }], + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, `@category:"${category.toLowerCase()}"`)) + }); + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.listBuiltInExtensions', + title: { value: localize('showBuiltInExtensions', "Show Built-in Extensions"), original: 'Show Built-in Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 1, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('builtin filter', "Built-in") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@builtin ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showInstalledExtensions', + title: { value: localize('showInstalledExtensions', "Show Installed Extensions"), original: 'Show Installed Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 2, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('installed filter', "Installed") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@installed ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showEnabledExtensions', + title: { value: localize('showEnabledExtensions', "Show Enabled Extensions"), original: 'Show Enabled Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 3, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('enabled filter', "Enabled") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@enabled ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showDisabledExtensions', + title: { value: localize('showDisabledExtensions', "Show Disabled Extensions"), original: 'Show Disabled Extensions' }, + category: ExtensionsLocalizedLabel, + + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 4, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('disabled filter', "Disabled") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@disabled ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.listOutdatedExtensions', + title: { value: localize('showOutdatedExtensions', "Show Outdated Extensions"), original: 'Show Outdated Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 5, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('outdated filter', "Outdated") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@outdated ')) + }); + + const extensionsSortSubMenu = new MenuId('extensionsSortSubMenu'); + MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { + submenu: extensionsSortSubMenu, + title: localize('sorty by', "Sort By"), + when: CONTEXT_HAS_GALLERY, + group: '4_sort', + order: 1, + }); + + [ + { id: 'installs', title: localize('sort by installs', "Install Count") }, + { id: 'rating', title: localize('sort by rating', "Rating") }, + { id: 'name', title: localize('sort by name', "Name") }, + { id: 'publishedDate', title: localize('sort by date', "Published Date") }, + ].map(({ id, title }, index) => { + this.registerExtensionAction({ + id: `extensions.sort.${id}`, + title, + precondition: DefaultViewsContext.toNegated(), + menu: [{ + id: extensionsSortSubMenu, + when: CONTEXT_HAS_GALLERY, + order: index, + }], + toggled: ExtensionsSortByContext.isEqualTo(id), + run: async () => { + const viewlet = await this.viewletService.openViewlet(VIEWLET_ID, true); + const extensionsViewPaneContainer = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer; + const currentQuery = Query.parse(extensionsViewPaneContainer.searchValue || ''); + extensionsViewPaneContainer.search(new Query(currentQuery.value, id, currentQuery.groupBy).toString()); + extensionsViewPaneContainer.focus(); + } + }); + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.clearExtensionsSearchResults', + title: { value: localize('clearExtensionsSearchResults', "Clear Extensions Search Results"), original: 'Clear Extensions Search Results' }, + category: ExtensionsLocalizedLabel, + icon: clearSearchResultsIcon, + f1: true, + precondition: DefaultViewsContext.toNegated(), + menu: { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: 'navigation', + order: 3, + }, + run: async (accessor: ServicesAccessor) => { + const viewPaneContainer = accessor.get(IViewsService).getActiveViewPaneContainerWithId(VIEWLET_ID); + if (viewPaneContainer) { + const extensionsViewPaneContainer = viewPaneContainer as IExtensionsViewPaneContainer; + extensionsViewPaneContainer.search(''); + extensionsViewPaneContainer.focus(); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowDisabledExtensionsAction.ID, - title: { value: ShowDisabledExtensionsAction.LABEL, original: 'Show Disabled Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowDisabledExtensionsAction, ShowDisabledExtensionsAction.ID, ShowDisabledExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.refreshExtension', + title: { value: localize('refreshExtension', "Refresh"), original: 'Refresh' }, + category: ExtensionsLocalizedLabel, + icon: refreshIcon, + f1: true, + menu: { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: 'navigation', + order: 2 + }, + run: async (accessor: ServicesAccessor) => { + const viewPaneContainer = accessor.get(IViewsService).getActiveViewPaneContainerWithId(VIEWLET_ID); + if (viewPaneContainer) { + await (viewPaneContainer as IExtensionsViewPaneContainer).refresh(); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowBuiltInExtensionsAction.ID, - title: { value: ShowBuiltInExtensionsAction.LABEL, original: 'Show Built-in Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowBuiltInExtensionsAction, ShowBuiltInExtensionsAction.ID, ShowBuiltInExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.installWorkspaceRecommendedExtensions', + title: localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), + icon: installWorkspaceRecommendedIcon, + menu: { + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', WORKSPACE_RECOMMENDATIONS_VIEW_ID), + group: 'navigation', + order: 1 + }, + run: async (accessor: ServicesAccessor) => { + const view = accessor.get(IViewsService).getActiveViewWithId(WORKSPACE_RECOMMENDATIONS_VIEW_ID) as IWorkspaceRecommendedExtensionsView; + return view.installWorkspaceRecommendations(); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: UpdateAllAction.ID, - title: { value: UpdateAllAction.LABEL, original: 'Update All Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL, false)); - } + this.registerExtensionAction({ + id: ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, + title: ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL, + icon: configureRecommendedIcon, + menu: [{ + id: MenuId.CommandPalette, + when: WorkbenchStateContext.notEqualsTo('empty'), + }, { + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', WORKSPACE_RECOMMENDATIONS_VIEW_ID), + group: 'navigation', + order: 2 + }], + run: () => runAction(this.instantiationService.createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL)) }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: InstallVSIXAction.ID, - title: { value: InstallVSIXAction.LABEL, original: 'Install from VSIX...' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL)); - } + this.registerExtensionAction({ + id: InstallSpecificVersionOfExtensionAction.ID, + title: { value: InstallSpecificVersionOfExtensionAction.LABEL, original: 'Install Specific Version of Extension...' }, + category: ExtensionsLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, + run: () => runAction(this.instantiationService.createInstance(InstallSpecificVersionOfExtensionAction, InstallSpecificVersionOfExtensionAction.ID, InstallSpecificVersionOfExtensionAction.LABEL)) }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: DisableAllAction.ID, - title: { value: DisableAllAction.LABEL, original: 'Disable All Installed Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL, false)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: DisableAllWorkspaceAction.ID, - title: { value: DisableAllWorkspaceAction.LABEL, original: 'Disable All Installed Extensions for this Workspace' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([WorkbenchStateContext.notEqualsTo('empty'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(DisableAllWorkspaceAction, DisableAllWorkspaceAction.ID, DisableAllWorkspaceAction.LABEL, false)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: EnableAllAction.ID, - title: { value: EnableAllAction.LABEL, original: 'Enable All Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL, false)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: EnableAllWorkspaceAction.ID, - title: { value: EnableAllWorkspaceAction.LABEL, original: 'Enable All Extensions for this Workspace' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([WorkbenchStateContext.notEqualsTo('empty'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(EnableAllWorkspaceAction, EnableAllWorkspaceAction.ID, EnableAllWorkspaceAction.LABEL, false)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: CheckForUpdatesAction.ID, - title: { value: CheckForUpdatesAction.LABEL, original: 'Check for Extension Updates' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: ClearExtensionsSearchResultsAction.ID, - title: { value: ClearExtensionsSearchResultsAction.LABEL, original: 'Clear Extensions Search Results' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ClearExtensionsSearchResultsAction, ClearExtensionsSearchResultsAction.ID, ClearExtensionsSearchResultsAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: RefreshExtensionsAction.ID, - title: { value: RefreshExtensionsAction.LABEL, original: 'Refresh' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(RefreshExtensionsAction, RefreshExtensionsAction.ID, RefreshExtensionsAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: EnableAutoUpdateAction.ID, - title: { value: EnableAutoUpdateAction.LABEL, original: 'Enable Auto Updating Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: DisableAutoUpdateAction.ID, - title: { value: DisableAutoUpdateAction.LABEL, original: 'Disable Auto Updating Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: InstallSpecificVersionOfExtensionAction.ID, - title: { value: InstallSpecificVersionOfExtensionAction.LABEL, original: 'Install Specific Version of Extension...' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(InstallSpecificVersionOfExtensionAction, InstallSpecificVersionOfExtensionAction.ID, InstallSpecificVersionOfExtensionAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: ReinstallAction.ID, - title: { value: ReinstallAction.LABEL, original: 'Reinstall Extension...' }, - category: CATEGORIES.Developer, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ReinstallAction, ReinstallAction.ID, ReinstallAction.LABEL)); - } + this.registerExtensionAction({ + id: ReinstallAction.ID, + title: { value: ReinstallAction.LABEL, original: 'Reinstall Extension...' }, + category: CATEGORIES.Developer, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER])]) + }, + run: () => runAction(this.instantiationService.createInstance(ReinstallAction, ReinstallAction.ID, ReinstallAction.LABEL)) }); } // Extension Context Menu private registerContextMenuActions(): void { - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.copyExtension', - title: { value: localize('workbench.extensions.action.copyExtension', "Copy"), original: 'Copy' }, - menu: { - id: MenuId.ExtensionContext, - group: '1_copy' - } - }); - } - - async run(accessor: ServicesAccessor, extensionId: string) { - const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); - let extension = extensionWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] - || (await extensionWorkbenchService.queryGallery({ names: [extensionId], pageSize: 1 }, CancellationToken.None)).firstPage[0]; + this.registerExtensionAction({ + id: 'workbench.extensions.action.copyExtension', + title: { value: localize('workbench.extensions.action.copyExtension', "Copy"), original: 'Copy' }, + menu: { + id: MenuId.ExtensionContext, + group: '1_copy' + }, + run: async (accessor: ServicesAccessor, extensionId: string) => { + const clipboardService = accessor.get(IClipboardService); + let extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] + || (await this.extensionsWorkbenchService.queryGallery({ names: [extensionId], pageSize: 1 }, CancellationToken.None)).firstPage[0]; if (extension) { const name = localize('extensionInfoName', 'Name: {0}', extension.displayName); const id = localize('extensionInfoId', 'Id: {0}', extensionId); @@ -886,165 +1040,104 @@ class ExtensionsContributions implements IWorkbenchContribution { const publisher = localize('extensionInfoPublisher', 'Publisher: {0}', extension.publisherDisplayName); const link = extension.url ? localize('extensionInfoVSMarketplaceLink', 'VS Marketplace Link: {0}', `${extension.url}`) : null; const clipboardStr = `${name}\n${id}\n${description}\n${verision}\n${publisher}${link ? '\n' + link : ''}`; - await accessor.get(IClipboardService).writeText(clipboardStr); + await clipboardService.writeText(clipboardStr); } } }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.copyExtensionId', - title: { value: localize('workbench.extensions.action.copyExtensionId', "Copy Extension Id"), original: 'Copy Extension Id' }, - menu: { - id: MenuId.ExtensionContext, - group: '1_copy' - } - }); - } - - async run(accessor: ServicesAccessor, id: string) { - await accessor.get(IClipboardService).writeText(id); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.copyExtensionId', + title: { value: localize('workbench.extensions.action.copyExtensionId', "Copy Extension Id"), original: 'Copy Extension Id' }, + menu: { + id: MenuId.ExtensionContext, + group: '1_copy' + }, + run: async (accessor: ServicesAccessor, id: string) => accessor.get(IClipboardService).writeText(id) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.configure', - title: { value: localize('workbench.extensions.action.configure', "Extension Settings"), original: 'Extension Settings' }, - menu: { - id: MenuId.ExtensionContext, - group: '2_configure', - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('extensionHasConfiguration')) - } - }); - } - - async run(accessor: ServicesAccessor, id: string) { - await accessor.get(IPreferencesService).openSettings(false, `@ext:${id}`); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.configure', + title: { value: localize('workbench.extensions.action.configure', "Extension Settings"), original: 'Extension Settings' }, + menu: { + id: MenuId.ExtensionContext, + group: '2_configure', + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('extensionHasConfiguration')) + }, + run: async (accessor: ServicesAccessor, id: string) => accessor.get(IPreferencesService).openSettings(false, `@ext:${id}`) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: TOGGLE_IGNORE_EXTENSION_ACTION_ID, - title: { value: localize('workbench.extensions.action.toggleIgnoreExtension', "Sync This Extension"), original: `Sync This Extension` }, - menu: { - id: MenuId.ExtensionContext, - group: '2_configure', - when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.has('inExtensionEditor').negate()) - }, - }); - } - - async run(accessor: ServicesAccessor, id: string) { - const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); - const extension = extensionsWorkbenchService.local.find(e => areSameExtensions({ id }, e.identifier)); + this.registerExtensionAction({ + id: TOGGLE_IGNORE_EXTENSION_ACTION_ID, + title: { value: localize('workbench.extensions.action.toggleIgnoreExtension', "Sync This Extension"), original: `Sync This Extension` }, + menu: { + id: MenuId.ExtensionContext, + group: '2_configure', + when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.has('inExtensionEditor').negate()) + }, + run: async (accessor: ServicesAccessor, id: string) => { + const extension = this.extensionsWorkbenchService.local.find(e => areSameExtensions({ id }, e.identifier)); if (extension) { - return extensionsWorkbenchService.toggleExtensionIgnoredToSync(extension); + return this.extensionsWorkbenchService.toggleExtensionIgnoredToSync(extension); } } }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.ignoreRecommendation', - title: { value: localize('workbench.extensions.action.ignoreRecommendation', "Ignore Recommendation"), original: `Ignore Recommendation` }, - menu: { - id: MenuId.ExtensionContext, - group: '3_recommendations', - when: ContextKeyExpr.has('isExtensionRecommended'), - order: 1 - }, - }); - } - - async run(accessor: ServicesAccessor, id: string): Promise { - accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, true); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.ignoreRecommendation', + title: { value: localize('workbench.extensions.action.ignoreRecommendation', "Ignore Recommendation"), original: `Ignore Recommendation` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.has('isExtensionRecommended'), + order: 1 + }, + run: async (accessor: ServicesAccessor, id: string) => accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, true) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.undoIgnoredRecommendation', - title: { value: localize('workbench.extensions.action.undoIgnoredRecommendation', "Undo Ignored Recommendation"), original: `Undo Ignored Recommendation` }, - menu: { - id: MenuId.ExtensionContext, - group: '3_recommendations', - when: ContextKeyExpr.has('isUserIgnoredRecommendation'), - order: 1 - }, - }); - } - - async run(accessor: ServicesAccessor, id: string): Promise { - accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, false); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.undoIgnoredRecommendation', + title: { value: localize('workbench.extensions.action.undoIgnoredRecommendation', "Undo Ignored Recommendation"), original: `Undo Ignored Recommendation` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.has('isUserIgnoredRecommendation'), + order: 1 + }, + run: async (accessor: ServicesAccessor, id: string) => accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, false) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addExtensionToWorkspaceRecommendations', - title: { value: localize('workbench.extensions.action.addExtensionToWorkspaceRecommendations', "Add to Workspace Recommendations"), original: `Add to Workspace Recommendations` }, - menu: { - id: MenuId.ExtensionContext, - group: '3_recommendations', - when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended').negate(), ContextKeyExpr.has('isUserIgnoredRecommendation').negate()), - order: 2 - }, - }); - } - - run(accessor: ServicesAccessor, id: string): Promise { - return accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.addExtensionToWorkspaceRecommendations', + title: { value: localize('workbench.extensions.action.addExtensionToWorkspaceRecommendations', "Add to Workspace Recommendations"), original: `Add to Workspace Recommendations` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended').negate(), ContextKeyExpr.has('isUserIgnoredRecommendation').negate()), + order: 2 + }, + run: (accessor: ServicesAccessor, id: string) => accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', - title: { value: localize('workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', "Remove from Workspace Recommendations"), original: `Remove from Workspace Recommendations` }, - menu: { - id: MenuId.ExtensionContext, - group: '3_recommendations', - when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended')), - order: 2 - }, - }); - } - - run(accessor: ServicesAccessor, id: string): Promise { - return accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', + title: { value: localize('workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', "Remove from Workspace Recommendations"), original: `Remove from Workspace Recommendations` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended')), + order: 2 + }, + run: (accessor: ServicesAccessor, id: string) => accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addToWorkspaceRecommendations', - title: { value: localize('workbench.extensions.action.addToWorkspaceRecommendations', "Add Extension to Workspace Recommendations"), original: `Add Extension to Workspace Recommendations` }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), - }, - }); - } - + this.registerExtensionAction({ + id: 'workbench.extensions.action.addToWorkspaceRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceRecommendations', "Add Extension to Workspace Recommendations"), original: `Add Extension to Workspace Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, async run(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); const workpsaceExtensionsConfigService = accessor.get(IWorkpsaceExtensionsConfigService); @@ -1060,39 +1153,25 @@ class ExtensionsContributions implements IWorkbenchContribution { } }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addToWorkspaceFolderRecommendations', - title: { value: localize('workbench.extensions.action.addToWorkspaceFolderRecommendations', "Add Extension to Workspace Folder Recommendations"), original: `Add Extension to Workspace Folder Recommendations` }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), - }, - }); - } - - async run(accessor: ServicesAccessor): Promise { - return accessor.get(ICommandService).executeCommand('workbench.extensions.action.addToWorkspaceRecommendations'); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.addToWorkspaceFolderRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceFolderRecommendations', "Add Extension to Workspace Folder Recommendations"), original: `Add Extension to Workspace Folder Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, + run: () => this.commandService.executeCommand('workbench.extensions.action.addToWorkspaceRecommendations') }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addToWorkspaceIgnoredRecommendations', - title: { value: localize('workbench.extensions.action.addToWorkspaceIgnoredRecommendations', "Add Extension to Workspace Ignored Recommendations"), original: `Add Extension to Workspace Ignored Recommendations` }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), - }, - }); - } - + this.registerExtensionAction({ + id: 'workbench.extensions.action.addToWorkspaceIgnoredRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceIgnoredRecommendations', "Add Extension to Workspace Ignored Recommendations"), original: `Add Extension to Workspace Ignored Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, async run(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); const workpsaceExtensionsConfigService = accessor.get(IWorkpsaceExtensionsConfigService); @@ -1108,63 +1187,65 @@ class ExtensionsContributions implements IWorkbenchContribution { } }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', - title: { value: localize('workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', "Add Extension to Workspace Folder Ignored Recommendations"), original: `Add Extension to Workspace Folder Ignored Recommendations` }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), - }, - }); - } - - run(accessor: ServicesAccessor): Promise { - return accessor.get(ICommandService).executeCommand('workbench.extensions.action.addToWorkspaceIgnoredRecommendations'); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', "Add Extension to Workspace Folder Ignored Recommendations"), original: `Add Extension to Workspace Folder Ignored Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, + run: () => this.commandService.executeCommand('workbench.extensions.action.addToWorkspaceIgnoredRecommendations') }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: ConfigureWorkspaceRecommendedExtensionsAction.ID, - title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: WorkbenchStateContext.isEqualTo('workspace'), - }, - }); - } - - run(accessor: ServicesAccessor): Promise { - return accessor.get(IInstantiationService).createInstance(ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceRecommendedExtensionsAction.ID, ConfigureWorkspaceRecommendedExtensionsAction.LABEL).run(); - } + this.registerExtensionAction({ + id: ConfigureWorkspaceRecommendedExtensionsAction.ID, + title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: WorkbenchStateContext.isEqualTo('workspace'), + }, + run: () => runAction(this.instantiationService.createInstance(ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceRecommendedExtensionsAction.ID, ConfigureWorkspaceRecommendedExtensionsAction.LABEL)) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, - title: { value: ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace Folder)' }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: WorkbenchStateContext.notEqualsTo('empty'), - }, - }); - } - - run(accessor: ServicesAccessor): Promise { - return accessor.get(IInstantiationService).createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL).run(); - } - }); } + + private registerExtensionAction(extensionActionOptions: IExtensionActionOptions): IDisposable { + const menus = extensionActionOptions.menu ? isArray(extensionActionOptions.menu) ? extensionActionOptions.menu : [extensionActionOptions.menu] : []; + let menusWithOutTitles: ({ id: MenuId } & Omit)[] = []; + const menusWithTitles: { id: MenuId, item: IMenuItem }[] = []; + if (extensionActionOptions.menuTitles) { + for (let index = 0; index < menus.length; index++) { + const menu = menus[index]; + const menuTitle = extensionActionOptions.menuTitles[menu.id.id]; + if (menuTitle) { + menusWithTitles.push({ id: menu.id, item: { ...menu, command: { id: extensionActionOptions.id, title: menuTitle } } }); + } else { + menusWithOutTitles.push(menu); + } + } + } else { + menusWithOutTitles = menus; + } + const disposables = new DisposableStore(); + disposables.add(registerAction2(class extends Action2 { + constructor() { + super({ + ...extensionActionOptions, + menu: menusWithOutTitles + }); + } + run(accessor: ServicesAccessor, ...args: any[]): Promise { + return extensionActionOptions.run(accessor, ...args); + } + })); + if (menusWithTitles.length) { + disposables.add(MenuRegistry.appendMenuItems(menusWithTitles)); + } + return disposables; + } + } const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); @@ -1175,7 +1256,6 @@ workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(ExtensionActivationProgress, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(ExtensionDependencyChecker, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(RemoteExtensionsInstaller, LifecyclePhase.Eventually); // Running Extensions const actionRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 36dbf7c75..f12e706e8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -12,7 +12,7 @@ import { Event } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { dispose } from 'vs/base/common/lifecycle'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, AutoUpdateConfigurationKey, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { IGalleryExtension, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, INSTALL_ERROR_NOT_SUPPORTED, InstallOptions, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensioManagementService, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -20,9 +20,7 @@ import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; import { IFileService, IFileContent } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -40,12 +38,9 @@ import { MenuId, IMenuService } from 'vs/platform/actions/common/actions'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IQuickPickItem, IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { coalesce } from 'vs/base/common/arrays'; import { IWorkbenchThemeService, IWorkbenchTheme, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -53,9 +48,8 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { prefersExecuteOnUI, prefersExecuteOnWorkspace, canExecuteOnUI, canExecuteOnWorkspace, prefersExecuteOnWeb } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { IViewsService } from 'vs/workbench/common/views'; import { IActionViewItemOptions, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { EXTENSIONS_CONFIG, IExtensionsConfigContent } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors'; @@ -64,7 +58,7 @@ import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOpti import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; import { ILogService } from 'vs/platform/log/common/log'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; -import { clearSearchResultsIcon, infoIcon, manageExtensionIcon, refreshIcon, syncEnabledIcon, syncIgnoredIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { infoIcon, manageExtensionIcon, syncEnabledIcon, syncIgnoredIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; function getRelativeDateLabel(date: Date): string { const delta = new Date().getTime() - date.getTime(); @@ -100,17 +94,18 @@ function getRelativeDateLabel(date: Date): string { return ''; } -class PromptExtensionInstallFailureAction extends Action { +export class PromptExtensionInstallFailureAction extends Action { constructor( private readonly extension: IExtension, + private readonly version: string, private readonly installOperation: InstallOperation, private readonly error: Error, @IProductService private readonly productService: IProductService, @IOpenerService private readonly openerService: IOpenerService, @INotificationService private readonly notificationService: INotificationService, @IDialogService private readonly dialogService: IDialogService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @ICommandService private readonly commandService: ICommandService, @ILogService private readonly logService: ILogService, ) { super('extension.promptExtensionInstallFailure'); @@ -134,17 +129,13 @@ class PromptExtensionInstallFailureAction extends Action { if (this.extension.gallery && this.productService.extensionsGallery) { promptChoices.push({ label: localize('download', "Try Downloading Manually..."), - run: () => this.openerService.open(URI.parse(`${this.productService.extensionsGallery!.serviceUrl}/publishers/${this.extension.publisher}/vsextensions/${this.extension.name}/${this.extension.version}/vspackage`)).then(() => { + run: () => this.openerService.open(URI.parse(`${this.productService.extensionsGallery!.serviceUrl}/publishers/${this.extension.publisher}/vsextensions/${this.extension.name}/${this.version}/vspackage`)).then(() => { this.notificationService.prompt( Severity.Info, localize('install vsix', 'Once downloaded, please manually install the downloaded VSIX of \'{0}\'.', this.extension.identifier.id), [{ - label: InstallVSIXAction.LABEL, - run: () => { - const action = this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL); - action.run(); - action.dispose(); - } + label: localize('installVSIX', "Install from VSIX..."), + run: () => this.commandService.executeCommand(SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID) }] ); }) @@ -287,7 +278,7 @@ export abstract class AbstractInstallAction extends ExtensionAction { try { return await this.extensionsWorkbenchService.install(extension, this.getInstallOptions()); } catch (error) { - await this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, InstallOperation.Install, error).run(); + await this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Install, error).run(); return undefined; } } @@ -487,7 +478,7 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { || !this.extension.local || this.extension.state !== ExtensionState.Installed || this.extension.type !== ExtensionType.User - || this.extension.enablementState === EnablementState.DisabledByEnvironemt + || this.extension.enablementState === EnablementState.DisabledByEnvironment ) { return false; } @@ -715,7 +706,7 @@ export class UpdateAction extends ExtensionAction { await this.extensionsWorkbenchService.install(extension); alert(localize('updateExtensionComplete', "Updating extension {0} to version {1} completed.", extension.displayName, extension.latestVersion)); } catch (err) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, InstallOperation.Update, err).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, err).run(); } } @@ -775,7 +766,8 @@ export class ExtensionActionWithDropdownActionViewItem extends ActionWithDropdow updateClass(): void { super.updateClass(); - if (this.dropdownMenuActionViewItem && this.dropdownMenuActionViewItem.element) { + if (this.element && this.dropdownMenuActionViewItem && this.dropdownMenuActionViewItem.element) { + this.element.classList.toggle('empty', (this._action).menuActions.length === 0); this.dropdownMenuActionViewItem.element.classList.toggle('hide', (this._action).menuActions.length === 0); } } @@ -1063,7 +1055,7 @@ export class InstallAnotherVersionAction extends ExtensionAction { await this.extensionsWorkbenchService.installVersion(this.extension!, pick.id); } } catch (error) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension!, InstallOperation.Install, error).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension!, pick.latest ? this.extension!.latestVersion : pick.id, InstallOperation.Install, error).run(); } } return null; @@ -1246,140 +1238,6 @@ export class DisableDropDownAction extends ActionWithDropDownAction { } -export class CheckForUpdatesAction extends Action { - - static readonly ID = 'workbench.extensions.action.checkForUpdates'; - static readonly LABEL = localize('checkForUpdates', "Check for Extension Updates"); - - constructor( - id = CheckForUpdatesAction.ID, - label = CheckForUpdatesAction.LABEL, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @IViewletService private readonly viewletService: IViewletService, - @INotificationService private readonly notificationService: INotificationService - ) { - super(id, label, '', true); - } - - private checkUpdatesAndNotify(): void { - const outdated = this.extensionsWorkbenchService.outdated; - if (!outdated.length) { - this.notificationService.info(localize('noUpdatesAvailable', "All extensions are up to date.")); - return; - } - - let msgAvailableExtensions = outdated.length === 1 ? localize('singleUpdateAvailable', "An extension update is available.") : localize('updatesAvailable', "{0} extension updates are available.", outdated.length); - - const disabledExtensionsCount = outdated.filter(ext => ext.local && !this.extensionEnablementService.isEnabled(ext.local)).length; - if (disabledExtensionsCount) { - if (outdated.length === 1) { - msgAvailableExtensions = localize('singleDisabledUpdateAvailable', "An update to an extension which is disabled is available."); - } else if (disabledExtensionsCount === 1) { - msgAvailableExtensions = localize('updatesAvailableOneDisabled', "{0} extension updates are available. One of them is for a disabled extension.", outdated.length); - } else if (disabledExtensionsCount === outdated.length) { - msgAvailableExtensions = localize('updatesAvailableAllDisabled', "{0} extension updates are available. All of them are for disabled extensions.", outdated.length); - } else { - msgAvailableExtensions = localize('updatesAvailableIncludingDisabled', "{0} extension updates are available. {1} of them are for disabled extensions.", outdated.length, disabledExtensionsCount); - } - } - - this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => viewlet.search('')); - - this.notificationService.info(msgAvailableExtensions); - } - - run(): Promise { - return this.extensionsWorkbenchService.checkForUpdates().then(() => this.checkUpdatesAndNotify()); - } -} - -export class ToggleAutoUpdateAction extends Action { - - constructor( - id: string, - label: string, - private autoUpdateValue: boolean, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label, '', true); - this.updateEnablement(); - configurationService.onDidChangeConfiguration(() => this.updateEnablement()); - } - - private updateEnablement(): void { - this.enabled = this.configurationService.getValue(AutoUpdateConfigurationKey) !== this.autoUpdateValue; - } - - run(): Promise { - return this.configurationService.updateValue(AutoUpdateConfigurationKey, this.autoUpdateValue); - } -} - -export class EnableAutoUpdateAction extends ToggleAutoUpdateAction { - - static readonly ID = 'workbench.extensions.action.enableAutoUpdate'; - static readonly LABEL = localize('enableAutoUpdate', "Enable Auto Updating Extensions"); - - constructor( - id = EnableAutoUpdateAction.ID, - label = EnableAutoUpdateAction.LABEL, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, true, configurationService); - } -} - -export class DisableAutoUpdateAction extends ToggleAutoUpdateAction { - - static readonly ID = 'workbench.extensions.action.disableAutoUpdate'; - static readonly LABEL = localize('disableAutoUpdate', "Disable Auto Updating Extensions"); - - constructor( - id = EnableAutoUpdateAction.ID, - label = EnableAutoUpdateAction.LABEL, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, false, configurationService); - } -} - -export class UpdateAllAction extends Action { - - static readonly ID = 'workbench.extensions.action.updateAllExtensions'; - static readonly LABEL = localize('updateAll', "Update All Extensions"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - ) { - super(id, label, '', false); - - if (isPrimary) { - this._register(this.extensionsWorkbenchService.onChange(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - get enabled(): boolean { - return this.extensionsWorkbenchService.outdated.length > 0; - } - - run(): Promise { - return Promise.all(this.extensionsWorkbenchService.outdated.map(e => this.install(e))); - } - - private async install(extension: IExtension): Promise { - try { - await this.extensionsWorkbenchService.install(extension); - } catch (err) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, InstallOperation.Update, err).run(); - } - } -} - export class ReloadAction extends ExtensionAction { private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} reload`; @@ -1722,296 +1580,6 @@ export class SetProductIconThemeAction extends ExtensionAction { } } -export class OpenExtensionsViewletAction extends ShowViewletAction { - - static ID = VIEWLET_ID; - static LABEL = localize('toggleExtensionsViewlet', "Show Extensions"); - - constructor( - id: string, - label: string, - @IViewletService viewletService: IViewletService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, VIEWLET_ID, viewletService, editorGroupService, layoutService); - } -} - -export class InstallExtensionsAction extends OpenExtensionsViewletAction { - static ID = 'workbench.extensions.action.installExtensions'; - static LABEL = localize('installExtensions', "Install Extensions"); -} - -export class ShowEnabledExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showEnabledExtensions'; - static readonly LABEL = localize('showEnabledExtensions', "Show Enabled Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@enabled '); - viewlet.focus(); - }); - } -} - -export class ShowInstalledExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showInstalledExtensions'; - static readonly LABEL = localize('showInstalledExtensions', "Show Installed Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@installed '); - viewlet.focus(); - }); - } -} - -export class ShowDisabledExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showDisabledExtensions'; - static readonly LABEL = localize('showDisabledExtensions', "Show Disabled Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, 'null', true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@disabled '); - viewlet.focus(); - }); - } -} - -export class ClearExtensionsSearchResultsAction extends Action { - - static readonly ID = 'workbench.extensions.action.clearExtensionsSearchResults'; - static readonly LABEL = localize('clearExtensionsSearchResults', "Clear Extensions Search Results"); - - constructor( - id: string, - label: string, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label, ThemeIcon.asClassName(clearSearchResultsIcon), true); - } - - async run(): Promise { - const viewPaneContainer = this.viewsService.getActiveViewPaneContainerWithId(VIEWLET_ID); - if (viewPaneContainer) { - const extensionsViewPaneContainer = viewPaneContainer as IExtensionsViewPaneContainer; - extensionsViewPaneContainer.search(''); - extensionsViewPaneContainer.focus(); - } - } -} - -export class ClearExtensionsInputAction extends ClearExtensionsSearchResultsAction { - - constructor( - id: string, - label: string, - onSearchChange: Event, - getValue: () => string, - @IViewsService viewsService: IViewsService - ) { - super(id, label, viewsService); - this.onSearchChange(getValue()); - this._register(onSearchChange(this.onSearchChange, this)); - } - - private onSearchChange(value: string): void { - this.enabled = !!value; - } -} - -export class RefreshExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.refreshExtension'; - static readonly LABEL = localize('refreshExtension', "Refresh"); - - constructor( - id: string, - label: string, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label, ThemeIcon.asClassName(refreshIcon), true); - } - - async run(): Promise { - const viewPaneContainer = this.viewsService.getActiveViewPaneContainerWithId(VIEWLET_ID); - if (viewPaneContainer) { - const extensionsViewPaneContainer = viewPaneContainer as IExtensionsViewPaneContainer; - return extensionsViewPaneContainer.refresh(); - } - } -} - -export class ShowBuiltInExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.listBuiltInExtensions'; - static readonly LABEL = localize('showBuiltInExtensions', "Show Built-in Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@builtin '); - viewlet.focus(); - }); - } -} - -export class ShowOutdatedExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.listOutdatedExtensions'; - static readonly LABEL = localize('showOutdatedExtensions', "Show Outdated Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@outdated '); - viewlet.focus(); - }); - } -} - -export class ShowPopularExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showPopularExtensions'; - static readonly LABEL = localize('showPopularExtensions', "Show Popular Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@popular '); - viewlet.focus(); - }); - } -} - -export class PredefinedExtensionFilterAction extends Action { - - constructor( - id: string, - label: string, - private readonly filter: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search(`${this.filter} `); - viewlet.focus(); - }); - } -} - -export class RecentlyPublishedExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.recentlyPublishedExtensions'; - static readonly LABEL = localize('recentlyPublishedExtensions', "Recently Published Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@sort:publishedDate '); - viewlet.focus(); - }); - } -} - -export class ShowRecommendedExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showRecommendedExtensions'; - static readonly LABEL = localize('showRecommendedExtensions', "Show Recommended Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@recommended '); - viewlet.focus(); - }); - } -} - export class ShowRecommendedExtensionAction extends Action { static readonly ID = 'workbench.extensions.action.showRecommendedExtension'; @@ -2075,7 +1643,7 @@ export class InstallRecommendedExtensionAction extends Action { try { await this.extensionWorkbenchService.install(extension); } catch (err) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, InstallOperation.Install, err).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Install, err).run(); } } } @@ -2127,68 +1695,6 @@ export class UndoIgnoreExtensionRecommendationAction extends Action { } } -export class ShowRecommendedKeymapExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showRecommendedKeymapExtensions'; - static readonly LABEL = localize('showRecommendedKeymapExtensionsShort', "Keymaps"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@recommended:keymaps '); - viewlet.focus(); - }); - } -} - -export class ShowLanguageExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showLanguageExtensions'; - static readonly LABEL = localize('showLanguageExtensionsShort', "Language Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@category:"programming languages" @sort:installs '); - viewlet.focus(); - }); - } -} - -export class SearchCategoryAction extends Action { - - constructor( - id: string, - label: string, - private readonly category: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return new SearchExtensionsAction(`@category:"${this.category.toLowerCase()}"`, this.viewletService).run(); - } -} - export class SearchExtensionsAction extends Action { constructor( @@ -2205,46 +1711,6 @@ export class SearchExtensionsAction extends Action { } } -export class ChangeSortAction extends Action { - - private query: Query; - - constructor( - id: string, - label: string, - onSearchChange: Event, - private sortBy: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - - if (sortBy === undefined) { - throw new Error('bad arguments'); - } - - this.query = Query.parse(''); - this.enabled = false; - this.checked = false; - this._register(onSearchChange(this.onSearchChange, this)); - } - - private onSearchChange(value: string): void { - const query = Query.parse(value); - this.query = new Query(query.value, this.sortBy || query.sortBy, query.groupBy); - this.enabled = !!value && this.query.isValid(); - this.checked = this.enabled && this.query.equals(query); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search(this.query.toString()); - viewlet.focus(); - }); - } -} - export abstract class AbstractConfigureRecommendedExtensionsAction extends Action { constructor( @@ -2383,12 +1849,6 @@ export class ConfigureWorkspaceFolderRecommendedExtensionsAction extends Abstrac @ICommandService private readonly commandService: ICommandService ) { super(id, label, contextService, fileService, textFileService, editorService, jsonEditingService, textModelResolverService); - this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.update(), this)); - this.update(); - } - - private update(): void { - this.enabled = this.contextService.getWorkspace().folders.length > 0; } public run(): Promise { @@ -2735,156 +2195,6 @@ export class SystemDisabledWarningAction extends ExtensionAction { } } -export class DisableAllAction extends Action { - - static readonly ID = 'workbench.extensions.action.disableAll'; - static readonly LABEL = localize('disableAll', "Disable All Installed Extensions"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService - ) { - super(id, label); - if (isPrimary) { - this._register(this.extensionsWorkbenchService.onChange(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - private getExtensionsToDisable(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(e => !e.isBuiltin && !!e.local && this.extensionEnablementService.isEnabled(e.local) && this.extensionEnablementService.canChangeEnablement(e.local)); - } - - get enabled(): boolean { - return this.getExtensionsToDisable().length > 0; - } - - run(): Promise { - return this.extensionsWorkbenchService.setEnablement(this.getExtensionsToDisable(), EnablementState.DisabledGlobally); - } -} - -export class DisableAllWorkspaceAction extends Action { - - static readonly ID = 'workbench.extensions.action.disableAllWorkspace'; - static readonly LABEL = localize('disableAllWorkspace', "Disable All Installed Extensions for this Workspace"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService - ) { - super(id, label); - if (isPrimary) { - this._register(Event.any(this.workspaceContextService.onDidChangeWorkbenchState, this.extensionsWorkbenchService.onChange)(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - private getExtensionsToDisable(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(e => !e.isBuiltin && !!e.local && this.extensionEnablementService.isEnabled(e.local) && this.extensionEnablementService.canChangeEnablement(e.local)); - } - - get enabled(): boolean { - return this.getExtensionsToDisable().length > 0; - } - - run(): Promise { - return this.extensionsWorkbenchService.setEnablement(this.getExtensionsToDisable(), EnablementState.DisabledWorkspace); - } -} - -export class EnableAllAction extends Action { - - static readonly ID = 'workbench.extensions.action.enableAll'; - static readonly LABEL = localize('enableAll', "Enable All Extensions"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService - ) { - super(id, label); - if (isPrimary) { - this._register(this.extensionsWorkbenchService.onChange(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - private getExtensionsToEnable(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(e => !!e.local && this.extensionEnablementService.canChangeEnablement(e.local) && !this.extensionEnablementService.isEnabled(e.local)); - } - - get enabled(): boolean { - return this.getExtensionsToEnable().length > 0; - } - - run(): Promise { - return this.extensionsWorkbenchService.setEnablement(this.getExtensionsToEnable(), EnablementState.EnabledGlobally); - } -} - -export class EnableAllWorkspaceAction extends Action { - - static readonly ID = 'workbench.extensions.action.enableAllWorkspace'; - static readonly LABEL = localize('enableAllWorkspace', "Enable All Extensions for this Workspace"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService - ) { - super(id, label); - if (isPrimary) { - this._register(Event.any(this.workspaceContextService.onDidChangeWorkbenchState, this.extensionsWorkbenchService.onChange)(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - private getExtensionsToEnable(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(e => !!e.local && this.extensionEnablementService.canChangeEnablement(e.local) && !this.extensionEnablementService.isEnabled(e.local)); - } - - get enabled(): boolean { - return this.getExtensionsToEnable().length > 0; - } - - run(): Promise { - return this.extensionsWorkbenchService.setEnablement(this.getExtensionsToEnable(), EnablementState.EnabledWorkspace); - } -} - -export class InstallVSIXAction extends Action { - - static readonly ID = 'workbench.extensions.action.installVSIX'; - static readonly LABEL = localize('installVSIX', "Install from VSIX..."); - - constructor( - id = InstallVSIXAction.ID, - label = InstallVSIXAction.LABEL, - @IFileDialogService private readonly fileDialogService: IFileDialogService, - @ICommandService private readonly commandService: ICommandService - ) { - super(id, label, 'extension-action install-vsix', true); - } - - async run(): Promise { - const vsixPaths = await this.fileDialogService.showOpenDialog({ - title: localize('installFromVSIX', "Install from VSIX"), - filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }], - canSelectFiles: true, - canSelectMany: true, - openLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install")) - }); - - if (!vsixPaths) { - return; - } - - // Install extension(s), display notification(s), display @installed extensions - await this.commandService.executeCommand(INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, vsixPaths); - } -} - export class ReinstallAction extends Action { static readonly ID = 'workbench.extensions.action.reinstall'; @@ -2929,7 +2239,7 @@ export class ReinstallAction extends Action { } private reinstallExtension(extension: IExtension): Promise { - return this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL).run() + return this.instantiationService.createInstance(SearchExtensionsAction, '@installed ').run() .then(() => { return this.extensionsWorkbenchService.reinstall(extension) .then(extension => { @@ -3015,7 +2325,7 @@ export class InstallSpecificVersionOfExtensionAction extends Action { } private install(extension: IExtension, version: string): Promise { - return this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL).run() + return this.instantiationService.createInstance(SearchExtensionsAction, '@installed ').run() .then(() => { return this.extensionsWorkbenchService.installVersion(extension, version) .then(extension => { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index afe9ebc8a..9858def3b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -9,22 +9,17 @@ import { timeout, Delayer } from 'vs/base/common/async'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IAction, Action, Separator, SubmenuAction } from 'vs/base/common/actions'; +import { Event } from 'vs/base/common/event'; +import { Action } from 'vs/base/common/actions'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { append, $, Dimension, hide, show } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, AutoUpdateConfigurationKey, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID } from '../common/extensions'; -import { - ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, - EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction, SearchCategoryAction, - RecentlyPublishedExtensionsAction, ShowInstalledExtensionsAction, ShowOutdatedExtensionsAction, ShowDisabledExtensionsAction, - ShowEnabledExtensionsAction, PredefinedExtensionFilterAction, RefreshExtensionsAction -} from 'vs/workbench/contrib/extensions/browser/extensionsActions'; -import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, DefaultViewsContext, ExtensionsSortByContext, WORKSPACE_RECOMMENDATIONS_VIEW_ID } from '../common/extensions'; +import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInFeatureExtensionsView, BuiltInThemesExtensionsView, BuiltInProgrammingLanguageExtensionsView, ServerInstalledExtensionsView, DefaultRecommendedExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; @@ -32,24 +27,25 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import Severity from 'vs/base/common/severity'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IViewsRegistry, IViewDescriptor, Extensions, ViewContainer, IViewDescriptorService, IAddedViewDescriptorRef } from 'vs/workbench/common/views'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; import { SuggestEnabledInput, attachSuggestEnabledInputBoxStyler } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; -import { ExtensionType, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { ILabelService } from 'vs/platform/label/common/label'; import { MementoObject } from 'vs/workbench/common/memento'; @@ -62,10 +58,9 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { isWeb } from 'vs/base/common/platform'; -import { memoize } from 'vs/base/common/decorators'; -import { filterIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { installLocalInRemoteIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; -const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); const SearchMarketplaceExtensionsContext = new RawContextKey('searchMarketplaceExtensions', false); const SearchIntalledExtensionsContext = new RawContextKey('searchInstalledExtensions', false); const SearchOutdatedExtensionsContext = new RawContextKey('searchOutdatedExtensions', false); @@ -155,7 +150,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio get name() { return getInstalledViewName(); }, weight: 100, order: 1, - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('hasInstalledExtensions')), + when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.not('hasInstalledExtensions')), /* Empty installed extensions view shall have fixed height */ ctorDescriptor: new SyncDescriptor(ServerInstalledExtensionsView, [{ server, fixedHeight: true, onDidChangeTitle }]), /* Empty installed extensions views shall not be allowed to hidden */ @@ -168,11 +163,49 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio get name() { return getInstalledViewName(); }, weight: 100, order: 1, - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), isWebServer ? ContextKeyExpr.has('hasInstalledWebExtensions') : ContextKeyExpr.has('hasInstalledExtensions')), + when: ContextKeyExpr.and(DefaultViewsContext, isWebServer ? ContextKeyExpr.has('hasInstalledWebExtensions') : ContextKeyExpr.has('hasInstalledExtensions')), ctorDescriptor: new SyncDescriptor(ServerInstalledExtensionsView, [{ server, onDidChangeTitle }]), /* Installed extensions views shall not be allowed to hidden when there are more than one server */ canToggleVisibility: servers.length === 1 }); + + if (server === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer) { + registerAction2(class InstallLocalExtensionsInRemoteAction2 extends Action2 { + constructor() { + super({ + id: 'workbench.extensions.installLocalExtensions', + get title() { return localize('select and install local extensions', "Install Local Extensions in '{0}'...", server.label); }, + category: localize({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"), + icon: installLocalInRemoteIcon, + f1: true, + menu: { + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', id), + group: 'navigation', + } + }); + } + run(accessor: ServicesAccessor): Promise { + return accessor.get(IInstantiationService).createInstance(InstallLocalExtensionsInRemoteAction).run(); + } + }); + } + } + + if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { + registerAction2(class InstallRemoteExtensionsInLocalAction2 extends Action2 { + constructor() { + super({ + id: 'workbench.extensions.actions.installLocalExtensionsInRemote', + title: { value: localize('install remote in local', "Install Remote Extensions Locally..."), original: 'Install Remote Extensions Locally...' }, + category: localize({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"), + f1: true + }); + } + run(accessor: ServicesAccessor): Promise { + return accessor.get(IInstantiationService).createInstance(InstallRemoteExtensionsInLocalAction, 'workbench.extensions.actions.installLocalExtensionsInRemote').run(); + } + }); } /* @@ -184,7 +217,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id: 'workbench.views.extensions.popular', name: localize('popularExtensions', "Popular"), ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('hasInstalledExtensions')), + when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.not('hasInstalledExtensions')), weight: 60, order: 2, canToggleVisibility: false @@ -199,7 +232,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id: 'extensions.recommendedList', name: localize('recommendedExtensions', "Recommended"), ctorDescriptor: new SyncDescriptor(DefaultRecommendedExtensionsView, [{}]), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('config.extensions.showRecommendationsOnlyOnDemand')), + when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.not('config.extensions.showRecommendationsOnlyOnDemand')), weight: 40, order: 3, canToggleVisibility: true @@ -215,7 +248,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id: 'workbench.views.extensions.enabled', name: localize('enabledExtensions', "Enabled"), ctorDescriptor: new SyncDescriptor(EnabledExtensionsView, [{}]), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')), + when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.has('hasInstalledExtensions')), hideByDefault: true, weight: 40, order: 4, @@ -230,7 +263,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id: 'workbench.views.extensions.disabled', name: localize('disabledExtensions', "Disabled"), ctorDescriptor: new SyncDescriptor(DisabledExtensionsView, [{}]), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')), + when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.has('hasInstalledExtensions')), hideByDefault: true, weight: 10, order: 5, @@ -312,7 +345,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio const viewDescriptors: IViewDescriptor[] = []; viewDescriptors.push({ - id: 'workbench.views.extensions.workspaceRecommendations', + id: WORKSPACE_RECOMMENDATIONS_VIEW_ID, name: localize('workspaceRecommendedExtensions', "Workspace Recommendations"), ctorDescriptor: new SyncDescriptor(WorkspaceRecommendedExtensionsView, [{}]), when: ContextKeyExpr.and(ContextKeyExpr.has('recommendedExtensions'), WorkbenchStateContext.notEqualsTo('empty')), @@ -361,9 +394,8 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IExtensionsViewPaneContainer { - private readonly _onSearchChange: Emitter = this._register(new Emitter()); - private readonly onSearchChange: Event = this._onSearchChange.event; private defaultViewsContextKey: IContextKey; + private sortByContextKey: IContextKey; private searchMarketplaceExtensionsContextKey: IContextKey; private searchInstalledExtensionsContextKey: IContextKey; private searchOutdatedExtensionsContextKey: IContextKey; @@ -379,8 +411,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE private root: HTMLElement | undefined; private searchBox: SuggestEnabledInput | undefined; private readonly searchViewletState: MementoObject; - private readonly sortActions: ChangeSortAction[]; - private secondaryActions: IAction[] | undefined = undefined; constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @@ -390,7 +420,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @INotificationService private readonly notificationService: INotificationService, @IViewletService private readonly viewletService: IViewletService, @IThemeService themeService: IThemeService, @@ -408,6 +437,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchDelayer = new Delayer(500); this.defaultViewsContextKey = DefaultViewsContext.bindTo(contextKeyService); + this.sortByContextKey = ExtensionsSortByContext.bindTo(contextKeyService); this.searchMarketplaceExtensionsContextKey = SearchMarketplaceExtensionsContext.bindTo(contextKeyService); this.searchInstalledExtensionsContextKey = SearchIntalledExtensionsContext.bindTo(contextKeyService); this.searchOutdatedExtensionsContextKey = SearchOutdatedExtensionsContext.bindTo(contextKeyService); @@ -421,13 +451,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this._register(this.viewletService.onDidViewletOpen(this.onViewletOpen, this)); this.searchViewletState = this.getMemento(StorageScope.WORKSPACE, StorageTarget.USER); - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(AutoUpdateConfigurationKey)) { - this.secondaryActions = undefined; - this.updateTitleArea(); - } - }, this)); - if (extensionManagementServerService.webExtensionManagementServer) { this._register(extensionsWorkbenchService.onChange(() => { // show installed web extensions view only when it is not visible @@ -437,13 +460,10 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } })); } + } - this.sortActions = [ - this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.install', localize('sort by installs', "Install Count"), this.onSearchChange, 'installs')), - this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.rating', localize('sort by rating', "Rating"), this.onSearchChange, 'rating')), - this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.name', localize('sort by name', "Name"), this.onSearchChange, 'name')), - this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.publishedDate', localize('sort by date', "Published Date"), this.onSearchChange, 'publishedDate')), - ]; + get searchValue(): string | undefined { + return this.searchBox?.getValue(); } create(parent: HTMLElement): void { @@ -478,8 +498,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this._register(attachSuggestEnabledInputBoxStyler(this.searchBox, this.themeService)); this._register(this.searchBox.onInputDidChange(() => { + this.sortByContextKey.set(Query.parse(this.searchBox!.getValue() || '').sortBy); this.triggerSearch(); - this._onSearchChange.fire(this.searchBox!.getValue()); }, this)); this._register(this.searchBox.onShouldFocusResults(() => this.focusListView(), this)); @@ -556,59 +576,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE return 400; } - @memoize - getActions(): IAction[] { - // Local extensions filters - let filterActions: IAction[] = [ - this._register(this.instantiationService.createInstance(ShowBuiltInExtensionsAction, ShowBuiltInExtensionsAction.ID, localize('builtin filter', "Built-in"))), - this._register(this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, localize('installed filter', "Installed"))), - this._register(this.instantiationService.createInstance(ShowEnabledExtensionsAction, ShowEnabledExtensionsAction.ID, localize('enabled filter', "Enabled"))), - this._register(this.instantiationService.createInstance(ShowDisabledExtensionsAction, ShowDisabledExtensionsAction.ID, localize('disabled filter', "Disabled"))), - this._register(this.instantiationService.createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, localize('outdated filter', "Outdated"))), - ]; - - if (this.extensionGalleryService.isEnabled()) { - filterActions = [ - this._register(this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.featured', localize('featured filter', "Featured"), '@featured')), - this._register(this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.popular', localize('most popular filter', "Most Popular"), '@popular')), - this._register(this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.recommended', localize('most popular recommended', "Recommended"), '@recommended')), - this._register(this.instantiationService.createInstance(RecentlyPublishedExtensionsAction, RecentlyPublishedExtensionsAction.ID, localize('recently published filter', "Recently Published"))), - this._register(new Separator()), - this._register(new SubmenuAction('workbench.extensions.action.filterExtensionsByCategory', localize('filter by category', "Category"), EXTENSION_CATEGORIES.map(category => this.instantiationService.createInstance(SearchCategoryAction, `extensions.actions.searchByCategory.${category}`, category, category)))), - this._register(new Separator()), - ...filterActions, - this._register(new Separator()), - this._register(new SubmenuAction('workbench.extensions.action.sortBy', localize('sorty by', "Sort By"), this.sortActions)), - ]; - } - - return [ - this._register(new SubmenuAction('workbench.extensions.action.filterExtensions', localize('filterExtensions', "Filter Extensions..."), filterActions, ThemeIcon.asClassName(filterIcon))), - this._register(this.instantiationService.createInstance(RefreshExtensionsAction, RefreshExtensionsAction.ID, RefreshExtensionsAction.LABEL)), - this._register(this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange, () => this.searchBox!.getValue() || '')), - ]; - } - - getSecondaryActions(): IAction[] { - if (!this.secondaryActions) { - this.secondaryActions = []; - this.secondaryActions.push(this.instantiationService.createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL)); - if (this.configurationService.getValue(AutoUpdateConfigurationKey)) { - this.secondaryActions.push(this.instantiationService.createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL)); - } else { - this.secondaryActions.push(this.instantiationService.createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL, false), this.instantiationService.createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL)); - } - - this.secondaryActions.push(new Separator()); - this.secondaryActions.push(this.instantiationService.createInstance(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL, false)); - this.secondaryActions.push(this.instantiationService.createInstance(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL, false)); - - this.secondaryActions.push(new Separator()); - this.secondaryActions.push(this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL)); - } - return this.secondaryActions; - } - search(value: string): void { if (this.searchBox && this.searchBox.getValue() !== value) { this.searchBox.setValue(value); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 932a76b68..be8f1ac3c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -17,19 +17,19 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { append, $ } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Delegate, Renderer, IExtensionsViewState, EXTENSION_LIST_ELEMENT_HEIGHT } from 'vs/workbench/contrib/extensions/browser/extensionsList'; -import { ExtensionState, IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ExtensionState, IExtension, IExtensionsWorkbenchService, IWorkspaceRecommendedExtensionsView } from 'vs/workbench/contrib/extensions/common/extensions'; import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; -import { ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { ManageExtensionAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { WorkbenchPagedList, ListResourceNavigator } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { coalesce, distinct, flatten } from 'vs/base/common/arrays'; import { IExperimentService, IExperiment, ExperimentActionType } from 'vs/workbench/contrib/experiments/common/experimentService'; @@ -49,7 +49,6 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { configureRecommendedIcon, installLocalInRemoteIcon, installWorkspaceRecommendedIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; // Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result']; @@ -134,8 +133,12 @@ export class ExtensionsListView extends ViewPane { if (this.options.onDidChangeTitle) { this._register(this.options.onDidChangeTitle(title => this.updateTitle(title))); } + + this.registerActions(); } + protected registerActions(): void { } + protected renderHeader(container: HTMLElement): void { container.classList.add('extension-view-header'); super.renderHeader(container); @@ -263,32 +266,27 @@ export class ExtensionsListView extends ViewPane { const runningExtensions = await this.extensionService.getExtensions(); const manageExtensionAction = this.instantiationService.createInstance(ManageExtensionAction); manageExtensionAction.extension = e.element; + let groups: IAction[][] = []; if (manageExtensionAction.enabled) { - const groups = await manageExtensionAction.getActionGroups(runningExtensions); - let actions: IAction[] = []; - for (const menuActions of groups) { - actions = [...actions, ...menuActions, new Separator()]; - } - this.contextMenuService.showContextMenu({ - getAnchor: () => e.anchor, - getActions: () => actions.slice(0, actions.length - 1) - }); + groups = await manageExtensionAction.getActionGroups(runningExtensions); + } else if (e.element) { - const groups = getContextMenuActions(e.element, false, this.instantiationService); + groups = getContextMenuActions(e.element, false, this.instantiationService); groups.forEach(group => group.forEach(extensionAction => { if (extensionAction instanceof ExtensionAction) { extensionAction.extension = e.element!; } })); - let actions: IAction[] = []; - for (const menuActions of groups) { - actions = [...actions, ...menuActions, new Separator()]; - } - this.contextMenuService.showContextMenu({ - getAnchor: () => e.anchor, - getActions: () => actions - }); } + let actions: IAction[] = []; + for (const menuActions of groups) { + actions = [...actions, ...menuActions, new Separator()]; + } + actions.pop(); + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions + }); } } @@ -693,8 +691,14 @@ export class ExtensionsListView extends ViewPane { } // All recommendations - if (/@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)) { - return this.getAllRecommendationsModel(query, options, token); + if (/@recommended:all/i.test(query.value)) { + return this.getAllRecommendationsModel(options, token); + } + + // Search recommendations + if (ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value) || + (ExtensionsListView.isRecommendedExtensionsQuery(query.value) && options.sortBy !== undefined)) { + return this.searchRecommendations(query, options, token); } // Other recommendations @@ -730,10 +734,8 @@ export class ExtensionsListView extends ViewPane { } private async getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended:workspace/g, '').trim().toLowerCase(); const recommendations = await this.getWorkspaceRecommendations(); - const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token)) - .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token)); this.telemetryService.publicLog2<{ count: number }, WorkspaceRecommendationsClassification>('extensionWorkspaceRecommendations:open', { count: installableRecommendations.length }); const result: IExtension[] = coalesce(recommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); return new PagedModel(result); @@ -755,14 +757,19 @@ export class ExtensionsListView extends ViewPane { } private async getOtherRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended/g, '').trim().toLowerCase(); + const otherRecommendations = await this.getOtherRecommendations(); + const installableRecommendations = await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token); + const result = coalesce(otherRecommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); + return new PagedModel(result); + } + private async getOtherRecommendations(): Promise { const local = (await this.extensionsWorkbenchService.queryLocal(this.options.server)) .map(e => e.identifier.id.toLowerCase()); const workspaceRecommendations = (await this.getWorkspaceRecommendations()) .map(extensionId => extensionId.toLowerCase()); - const otherRecommendations = distinct( + return distinct( flatten(await Promise.all([ // Order is important this.extensionRecommendationsService.getImportantRecommendations(), @@ -770,16 +777,10 @@ export class ExtensionsListView extends ViewPane { this.extensionRecommendationsService.getOtherRecommendations() ])).filter(extensionId => !local.includes(extensionId.toLowerCase()) && !workspaceRecommendations.includes(extensionId.toLowerCase()) ), extensionId => extensionId.toLowerCase()); - - const installableRecommendations = (await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token)) - .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); - - const result: IExtension[] = coalesce(otherRecommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); - return new PagedModel(result); } // Get All types of recommendations, trimmed to show a max of 8 at any given time - private async getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + private async getAllRecommendationsModel(options: IQueryOptions, token: CancellationToken): Promise> { const local = (await this.extensionsWorkbenchService.queryLocal(this.options.server)).map(e => e.identifier.id.toLowerCase()); const allRecommendations = distinct( @@ -797,6 +798,15 @@ export class ExtensionsListView extends ViewPane { return new PagedModel(result.slice(0, 8)); } + private async searchRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const value = query.value.replace(/@recommended/g, '').trim().toLowerCase(); + const recommendations = distinct([...await this.getWorkspaceRecommendations(), ...await this.getOtherRecommendations()]); + const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations', sortBy: undefined }, token)) + .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + const result = coalesce(recommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); + return new PagedModel(this.sortExtensions(result, options)); + } + // Sorts the firstPage of the pager in the same order as given array of extension ids private sortFirstPage(pager: IPager, ids: string[]) { ids = ids.map(x => x.toLowerCase()); @@ -951,7 +961,7 @@ export class ExtensionsListView extends ViewPane { } static isSearchRecommendedExtensionsQuery(query: string): boolean { - return /@recommended/i.test(query) && !ExtensionsListView.isRecommendedExtensionsQuery(query); + return /@recommended\s.+/i.test(query); } static isWorkspaceRecommendedExtensionsQuery(query: string): boolean { @@ -989,14 +999,6 @@ export class ServerInstalledExtensionsView extends ExtensionsListView { return super.show(query.trim()); } - getActions(): IAction[] { - if (this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer === this.options.server) { - const installLocalExtensionsInRemoteAction = this._register(this.instantiationService.createInstance(InstallLocalExtensionsInRemoteAction)); - installLocalExtensionsInRemoteAction.class = ThemeIcon.asClassName(installLocalInRemoteIcon); - return [installLocalExtensionsInRemoteAction]; - } - return []; - } } export class EnabledExtensionsView extends ExtensionsListView { @@ -1074,9 +1076,8 @@ export class RecommendedExtensionsView extends ExtensionsListView { } } -export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { +export class WorkspaceRecommendedExtensionsView extends ExtensionsListView implements IWorkspaceRecommendedExtensionsView { private readonly recommendedExtensionsQuery = '@recommended:workspace'; - private installAllAction: Action | undefined; renderBody(container: HTMLElement): void { super.renderBody(container); @@ -1085,31 +1086,13 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { this._register(this.contextService.onDidChangeWorkbenchState(() => this.show(this.recommendedExtensionsQuery))); } - getActions(): IAction[] { - if (!this.installAllAction) { - this.installAllAction = this._register(new Action('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), ThemeIcon.asClassName(installWorkspaceRecommendedIcon), false, () => this.installWorkspaceRecommendations())); - } - - const configureWorkspaceFolderAction = this._register(this.instantiationService.createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL)); - configureWorkspaceFolderAction.class = ThemeIcon.asClassName(configureRecommendedIcon); - return [this.installAllAction, configureWorkspaceFolderAction]; - } - async show(query: string): Promise> { let shouldShowEmptyView = query && query.trim() !== '@recommended' && query.trim() !== '@recommended:workspace'; let model = await (shouldShowEmptyView ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery)); this.setExpanded(model.length > 0); - await this.setRecommendationsToInstall(); return model; } - private async setRecommendationsToInstall(): Promise { - const installableRecommendations = await this.getInstallableWorkspaceRecommendations(); - if (this.installAllAction) { - this.installAllAction.enabled = installableRecommendations.length > 0; - } - } - private async getInstallableWorkspaceRecommendations() { const installed = (await this.extensionsWorkbenchService.queryLocal()) .filter(l => l.enablementState !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind @@ -1118,9 +1101,16 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { return this.getInstallableRecommendations(recommendations, { source: 'install-all-workspace-recommendations' }, CancellationToken.None); } - private async installWorkspaceRecommendations(): Promise { + async installWorkspaceRecommendations(): Promise { const installableRecommendations = await this.getInstallableWorkspaceRecommendations(); - await this.extensionManagementService.installExtensions(installableRecommendations.map(i => i.gallery!)); + if (installableRecommendations.length) { + await this.extensionManagementService.installExtensions(installableRecommendations.map(i => i.gallery!)); + } else { + this.notificationService.notify({ + severity: Severity.Info, + message: localize('no local extensions', "There are no extensions to install.") + }); + } } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 3566ca415..2f8ac0a8f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -22,7 +22,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { URI } from 'vs/base/common/uri'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/common/url'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; @@ -40,6 +40,7 @@ import { getExtensionKind } from 'vs/workbench/services/extensions/common/extens import { FileAccess } from 'vs/base/common/network'; import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; interface IExtensionStateProvider { (extension: Extension): T; @@ -492,6 +493,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private static readonly SyncPeriod = 1000 * 60 * 60 * 12; // 12 hours declare readonly _serviceBrand: undefined; + private hasOutdatedExtensionsContextKey: IContextKey; + private readonly localExtensions: Extensions | null = null; private readonly remoteExtensions: Extensions | null = null; private readonly webExtensions: Extensions | null = null; @@ -520,9 +523,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IModeService private readonly modeService: IModeService, @IIgnoredExtensionsManagementService private readonly extensionsSyncManagementService: IIgnoredExtensionsManagementService, @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @IContextKeyService contextKeyService: IContextKeyService ) { super(); + this.hasOutdatedExtensionsContextKey = HasOutdatedExtensionsContext.bindTo(contextKeyService); if (extensionManagementServerService.localExtensionManagementServer) { this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer, ext => this.getExtensionState(ext))); this._register(this.localExtensions.onChange(e => this._onChange.fire(e ? e.extension : undefined))); @@ -559,7 +564,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.eventuallySyncWithGallery(true); }); - this._register(this.onChange(() => this.updateActivity())); + this._register(this.onChange(() => { + this.updateContexts(); + this.updateActivity(); + })); } get local(): IExtension[] { @@ -1189,13 +1197,21 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return changed; } + private updateContexts(extension?: Extension): void { + if (extension && extension.outdated) { + this.hasOutdatedExtensionsContextKey.set(true); + } else { + this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0); + } + } + private _activityCallBack: ((value: void) => void) | null = null; private updateActivity(): void { if ((this.localExtensions && this.localExtensions.local.some(e => e.state === ExtensionState.Installing || e.state === ExtensionState.Uninstalling)) || (this.remoteExtensions && this.remoteExtensions.local.some(e => e.state === ExtensionState.Installing || e.state === ExtensionState.Uninstalling)) || (this.webExtensions && this.webExtensions.local.some(e => e.state === ExtensionState.Installing || e.state === ExtensionState.Uninstalling))) { if (!this._activityCallBack) { - this.progressService.withProgress({ location: ProgressLocation.Extensions }, () => new Promise(c => this._activityCallBack = c)); + this.progressService.withProgress({ location: ProgressLocation.Extensions }, () => new Promise(resolve => this._activityCallBack = resolve)); } } else { if (this._activityCallBack) { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index 0502ce085..8749cbe23 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -179,3 +179,7 @@ .extension-list-item > .details > .footer > .monaco-action-bar > .actions-container .extension-action.label { max-width: 150px; } + +.extension-list-item .footer .monaco-action-bar .action-item.action-dropdown-item.empty > .action-label { + margin-right: 4px; +} diff --git a/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts b/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts deleted file mode 100644 index 3a8caf10a..000000000 --- a/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts +++ /dev/null @@ -1,59 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { MenuRegistry, MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; -import { localize } from 'vs/nls'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; -import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; - -export class RemoteExtensionsInstaller extends Disposable implements IWorkbenchContribution { - - constructor( - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @ILabelService labelService: ILabelService, - @IInstantiationService instantiationService: IInstantiationService - ) { - super(); - if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { - const installLocalExtensionsInRemoteAction = instantiationService.createInstance(InstallLocalExtensionsInRemoteAction); - CommandsRegistry.registerCommand('workbench.extensions.installLocalExtensions', () => installLocalExtensionsInRemoteAction.run()); - let disposable = Disposable.None; - const appendMenuItem = () => { - disposable.dispose(); - disposable = MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: 'workbench.extensions.installLocalExtensions', - category: localize({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"), - title: installLocalExtensionsInRemoteAction.label - } - }); - }; - appendMenuItem(); - this._register(labelService.onDidChangeFormatters(e => appendMenuItem())); - this._register(toDisposable(() => disposable.dispose())); - - this._register(registerAction2(class InstallRemoteExtensionsInLocalAction2 extends Action2 { - constructor() { - super({ - id: 'workbench.extensions.actions.installLocalExtensionsInRemote', - title: { value: localize('install remote in local', "Install Remote Extensions Locally..."), original: 'Install Remote Extensions Locally...' }, - category: localize({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"), - f1: true - }); - } - run(accessor: ServicesAccessor): Promise { - return accessor.get(IInstantiationService).createInstance(InstallRemoteExtensionsInLocalAction, 'workbench.extensions.actions.installLocalExtensionsInRemote').run(); - } - })); - } - } - -} diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 3cfe2796c..127ae556d 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -13,15 +13,21 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; -import { IViewPaneContainer } from 'vs/workbench/common/views'; +import { IView, IViewPaneContainer } from 'vs/workbench/common/views'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const VIEWLET_ID = 'workbench.view.extensions'; export interface IExtensionsViewPaneContainer extends IViewPaneContainer { + readonly searchValue: string | undefined; search(text: string): void; refresh(): Promise; } +export interface IWorkspaceRecommendedExtensionsView extends IView { + installWorkspaceRecommendations(): Promise; +} + export const enum ExtensionState { Installing, Installed, @@ -145,5 +151,12 @@ export class ExtensionContainers extends Disposable { } } +export const WORKSPACE_RECOMMENDATIONS_VIEW_ID = 'workbench.views.extensions.workspaceRecommendations'; export const TOGGLE_IGNORE_EXTENSION_ACTION_ID = 'workbench.extensions.action.toggleIgnoreExtension'; +export const SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID = 'workbench.extensions.action.installVSIX'; export const INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID = 'workbench.extensions.command.installFromVSIX'; + +// Context Keys +export const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); +export const ExtensionsSortByContext = new RawContextKey('extensionsSortByValue', ''); +export const HasOutdatedExtensionsContext = new RawContextKey('hasOutdatedExtensions', false); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts index a472654f0..edf6d1001 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -21,7 +21,6 @@ import { ExtensionHostProfileService } from 'vs/workbench/contrib/extensions/ele import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionsAutoProfiler } from 'vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler'; -import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { OpenExtensionsFolderAction } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionsActions'; import { ExtensionsLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; @@ -62,11 +61,10 @@ const actionRegistry = Registry.as(WorkbenchActionExte class ExtensionsContributions implements IWorkbenchContribution { constructor( - @INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService, @IExtensionRecommendationNotificationService extensionRecommendationNotificationService: IExtensionRecommendationNotificationService, @ISharedProcessService sharedProcessService: ISharedProcessService, ) { - sharedProcessService.registerChannel('IExtensionRecommendationNotificationService', new ExtensionRecommendationNotificationServiceChannel(extensionRecommendationNotificationService)); + sharedProcessService.registerChannel('extensionRecommendationNotification', new ExtensionRecommendationNotificationServiceChannel(extensionRecommendationNotificationService)); const openExtensionsFolderActionDescriptor = SyncActionDescriptor.from(OpenExtensionsFolderAction); actionRegistry.registerWorkbenchAction(openExtensionsFolderActionDescriptor, 'Extensions: Open Extensions Folder', ExtensionsLabel); } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts b/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts index faf606a75..762b07c45 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts @@ -14,6 +14,8 @@ import { ExtensionType, IExtensionDescription } from 'vs/platform/extensions/com import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; +const builtinExtensionIssueUrl = 'https://github.com/microsoft/vscode'; + export class ReportExtensionIssueAction extends Action { private static readonly _id = 'workbench.extensions.action.reportExtensionIssue'; @@ -34,7 +36,8 @@ export class ReportExtensionIssueAction extends Action { @INativeHostService private readonly nativeHostService: INativeHostService ) { super(ReportExtensionIssueAction._id, ReportExtensionIssueAction._label, 'extension-action report-issue'); - this.enabled = !!extension.description.repository && !!extension.description.repository.url; + + this.enabled = extension.description.isBuiltin || (!!extension.description.repository && !!extension.description.repository.url); } async run(): Promise { @@ -51,6 +54,9 @@ export class ReportExtensionIssueAction extends Action { unresponsiveProfile?: IExtensionHostProfile }): Promise { let baseUrl = extension.marketplaceInfo && extension.marketplaceInfo.type === ExtensionType.User && extension.description.repository ? extension.description.repository.url : undefined; + if (!baseUrl && extension.description.isBuiltin) { + baseUrl = builtinExtensionIssueUrl; + } if (!!baseUrl) { baseUrl = `${baseUrl.indexOf('.git') !== -1 ? baseUrl.substr(0, baseUrl.length - 4) : baseUrl}/issues/new/`; } else { diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index ed8a92a98..c380c4c74 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -5,11 +5,7 @@ import * as sinon from 'sinon'; import * as assert from 'assert'; -import * as path from 'vs/base/common/path'; -import * as fs from 'fs'; -import * as os from 'os'; import * as uuid from 'vs/base/common/uuid'; -import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs'; import { IExtensionGalleryService, IGalleryExtensionAssets, IGalleryExtension, IExtensionManagementService, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, IExtensionTipsService @@ -47,8 +43,6 @@ import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService, ILogService } from 'vs/platform/log/common/log'; -import { Schemas } from 'vs/base/common/network'; -import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { IFileService } from 'vs/platform/files/common/files'; import { IProductService } from 'vs/platform/product/common/productService'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService'; @@ -62,6 +56,11 @@ import { IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/e import { ExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionIgnoredRecommendationsService'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationNotificationService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { joinPath } from 'vs/base/common/resources'; +import { VSBuffer } from 'vs/base/common/buffer'; const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -181,7 +180,6 @@ suite('ExtensionRecommendationsService Test', () => { let instantiationService: TestInstantiationService; let testConfigurationService: TestConfigurationService; let testObject: ExtensionRecommendationsService; - let parentResource: string; let installEvent: Emitter, didInstallEvent: Emitter, uninstallEvent: Emitter, @@ -203,6 +201,7 @@ suite('ExtensionRecommendationsService Test', () => { testConfigurationService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, testConfigurationService); instantiationService.stub(INotificationService, new TestNotificationService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IExtensionManagementService, >{ onInstallExtension: installEvent.event, onDidInstallExtension: didInstallEvent.event, @@ -278,34 +277,30 @@ suite('ExtensionRecommendationsService Test', () => { }); }); - teardown(done => { - (testObject).dispose(); - if (parentResource) { - rimraf(parentResource, RimRafMode.MOVE).then(done, done); - } else { - done(); - } - }); + teardown(() => (testObject).dispose()); function setUpFolderWorkspace(folderName: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): Promise { - const id = uuid.generateUuid(); - parentResource = path.join(os.tmpdir(), 'vsctests', id); - return setUpFolder(folderName, parentResource, recommendedExtensions, ignoredRecommendations); + return setUpFolder(folderName, recommendedExtensions, ignoredRecommendations); } - async function setUpFolder(folderName: string, parentDir: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): Promise { - const folderDir = path.join(parentDir, folderName); - const workspaceSettingsDir = path.join(folderDir, '.vscode'); - await mkdirp(workspaceSettingsDir, 493); - const configPath = path.join(workspaceSettingsDir, 'extensions.json'); - fs.writeFileSync(configPath, JSON.stringify({ + async function setUpFolder(folderName: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): Promise { + const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); + const logService = new NullLogService(); + const fileService = new FileService(logService); + const fileSystemProvider = new InMemoryFileSystemProvider(); + fileService.registerProvider(ROOT.scheme, fileSystemProvider); + + const folderDir = joinPath(ROOT, folderName); + const workspaceSettingsDir = joinPath(folderDir, '.vscode'); + await fileService.createFolder(workspaceSettingsDir); + const configPath = joinPath(workspaceSettingsDir, 'extensions.json'); + await fileService.writeFile(configPath, VSBuffer.fromString(JSON.stringify({ 'recommendations': recommendedExtensions, 'unwantedRecommendations': ignoredRecommendations, - }, null, '\t')); + }, null, '\t'))); + + const myWorkspace = testWorkspace(folderDir); - const myWorkspace = testWorkspace(URI.from({ scheme: 'file', path: folderDir })); - const fileService = new FileService(new NullLogService()); - fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); instantiationService.stub(IFileService, fileService); workspaceService = new TestContextService(myWorkspace); instantiationService.stub(IWorkspaceContextService, workspaceService); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index 40cf65cc2..bcef66bbd 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -33,7 +33,7 @@ import { NativeURLService } from 'vs/platform/url/common/urlService'; import { URI } from 'vs/base/common/uri'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; +import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentServiceImpl'; import { ExtensionIdentifier, IExtensionContributions, ExtensionType, IExtensionDescription, IExtension } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -53,6 +53,8 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSyncResourceEnablementService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; let instantiationService: TestInstantiationService; let installEvent: Emitter, @@ -77,6 +79,7 @@ async function setupTest() { instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IProgressService, ProgressService); instantiationService.stub(IProductService, {}); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService); instantiationService.stub(ISharedProcessService, TestSharedProcessService); @@ -885,83 +888,6 @@ suite('ExtensionsActions', () => { }); }); - test('Test UpdateAllAction when no installed extensions', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - - assert.ok(!testObject.enabled); - }); - - test('Test UpdateAllAction when installed extensions are not outdated', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a'), aLocalExtension('b')]); - return instantiationService.get(IExtensionsWorkbenchService).queryLocal() - .then(extensions => assert.ok(!testObject.enabled)); - }); - - test('Test UpdateAllAction when some installed extensions are outdated', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - const local = [aLocalExtension('a', { version: '1.0.1' }), aLocalExtension('b', { version: '1.0.1' }), aLocalExtension('c', { version: '1.0.1' })]; - const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', local); - return workbenchService.queryLocal() - .then(async () => { - instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: local[0].identifier, version: '1.0.2' }), aGalleryExtension('b', { identifier: local[1].identifier, version: '1.0.2' }), aGalleryExtension('c', local[2].manifest))); - assert.ok(!testObject.enabled); - return new Promise(c => { - testObject.onDidChange(() => { - if (testObject.enabled) { - c(); - } - }); - workbenchService.queryGallery(CancellationToken.None); - }); - }); - }); - - test('Test UpdateAllAction when some installed extensions are outdated and some outdated are being installed', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - const local = [aLocalExtension('a', { version: '1.0.1' }), aLocalExtension('b', { version: '1.0.1' }), aLocalExtension('c', { version: '1.0.1' })]; - const gallery = [aGalleryExtension('a', { identifier: local[0].identifier, version: '1.0.2' }), aGalleryExtension('b', { identifier: local[1].identifier, version: '1.0.2' }), aGalleryExtension('c', local[2].manifest)]; - const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', local); - return workbenchService.queryLocal() - .then(async () => { - instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...gallery)); - assert.ok(!testObject.enabled); - return new Promise(c => { - installEvent.fire({ identifier: local[0].identifier, gallery: gallery[0] }); - testObject.onDidChange(() => { - if (testObject.enabled) { - c(); - } - }); - workbenchService.queryGallery(CancellationToken.None); - }); - }); - }); - - test('Test UpdateAllAction when some installed extensions are outdated and all outdated are being installed', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - const local = [aLocalExtension('a', { version: '1.0.1' }), aLocalExtension('b', { version: '1.0.1' }), aLocalExtension('c', { version: '1.0.1' })]; - const gallery = [aGalleryExtension('a', { identifier: local[0].identifier, version: '1.0.2' }), aGalleryExtension('b', { identifier: local[1].identifier, version: '1.0.2' }), aGalleryExtension('c', local[2].manifest)]; - const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', local); - return workbenchService.queryLocal() - .then(() => { - instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...gallery)); - return workbenchService.queryGallery(CancellationToken.None) - .then(() => { - installEvent.fire({ identifier: local[0].identifier, gallery: gallery[0] }); - installEvent.fire({ identifier: local[1].identifier, gallery: gallery[1] }); - assert.ok(!testObject.enabled); - }); - }); - }); - - test(`RecommendToFolderAction`, () => { - // TODO: Implement test - }); - }); suite('ReloadAction', () => { diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index cde013f45..b1e00bb80 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -35,7 +35,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { SinonStub } from 'sinon'; import { IExperimentService, ExperimentState, ExperimentActionType, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; +import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentServiceImpl'; import { ExtensionType, IExtension, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -165,7 +165,8 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(IViewDescriptorService, { getViewLocationById(): ViewContainerLocation { return ViewContainerLocation.Sidebar; - } + }, + onDidChangeLocation: Event.None }); instantiationService.stub(IExtensionService, >{ diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index f4f5d8ee5..76a502062 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -36,7 +36,7 @@ import { URI } from 'vs/base/common/uri'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionType, IExtension, ExtensionKind } from 'vs/platform/extensions/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; +import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentServiceImpl'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -46,6 +46,8 @@ import { IExperimentService } from 'vs/workbench/contrib/experiments/common/expe import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService'; import { Schemas } from 'vs/base/common/network'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -72,6 +74,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService); instantiationService.stub(IURLService, NativeURLService); instantiationService.stub(ISharedProcessService, TestSharedProcessService); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IWorkspaceContextService, new TestContextService()); instantiationService.stub(IConfigurationService, >{ diff --git a/src/vs/workbench/contrib/externalUriOpener/common/configuration.ts b/src/vs/workbench/contrib/externalUriOpener/common/configuration.ts new file mode 100644 index 000000000..83bd5a398 --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/common/configuration.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IConfigurationNode, IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import * as nls from 'vs/nls'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { Registry } from 'vs/platform/registry/common/platform'; + +export const defaultExternalUriOpenerId = 'default'; + +export const externalUriOpenersSettingId = 'workbench.externalUriOpeners'; + +export interface ExternalUriOpenersConfiguration { + readonly [uriGlob: string]: string; +} + +const externalUriOpenerIdSchemaAddition: IJSONSchema = { + type: 'string', + enum: [] +}; + +const exampleUriPatterns = ` +- \`https://microsoft.com\`: Matches this specific domain using https +- \`https://microsoft.com:8080\`: Matches this specific domain on this port using https +- \`https://microsoft.com:*\`: Matches this specific domain on any port using https +- \`https://microsoft.com/foo\`: Matches \`https://microsoft.com/foo\` and \`https://microsoft.com/foo/bar\`, but not \`https://microsoft.com/foobar\` or \`https://microsoft.com/bar\` +- \`https://*.microsoft.com\`: Match all domains ending in \`microsoft.com\` using https +- \`microsoft.com\`: Match this specific domain using either http or https +- \`*.microsoft.com\`: Match all domains ending in \`microsoft.com\` using either http or https +- \`http://192.168.0.1\`: Matches this specific IP using http +- \`http://192.168.0.*\`: Matches all IP's with this prefix using http +- \`*\`: Match all domains using either http or https`; + +export const externalUriOpenersConfigurationNode: IConfigurationNode = { + ...workbenchConfigurationNodeBase, + properties: { + [externalUriOpenersSettingId]: { + type: 'object', + markdownDescription: nls.localize('externalUriOpeners', "Configure the opener to use for external uris (i.e. http, https)."), + defaultSnippets: [{ + body: { + 'example.com': '$1' + } + }], + additionalProperties: { + anyOf: [ + { + type: 'string', + markdownDescription: nls.localize('externalUriOpeners.uri', "Map uri pattern to an opener id.\nExample patterns: \n{0}", exampleUriPatterns), + }, + { + type: 'string', + markdownDescription: nls.localize('externalUriOpeners.uri', "Map uri pattern to an opener id.\nExample patterns: \n{0}", exampleUriPatterns), + enum: [defaultExternalUriOpenerId], + enumDescriptions: [nls.localize('externalUriOpeners.defaultId', "Open using VS Code's standard opener.")], + }, + externalUriOpenerIdSchemaAddition + ] + } + } + } +}; + +export function updateContributedOpeners(enumValues: string[], enumDescriptions: string[]): void { + externalUriOpenerIdSchemaAddition.enum = enumValues; + externalUriOpenerIdSchemaAddition.enumDescriptions = enumDescriptions; + + Registry.as(Extensions.Configuration) + .notifyConfigurationSchemaUpdated(externalUriOpenersConfigurationNode); +} diff --git a/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts b/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts new file mode 100644 index 000000000..457d5df30 --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Memento } from 'vs/workbench/common/memento'; +import { updateContributedOpeners } from 'vs/workbench/contrib/externalUriOpener/common/configuration'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +interface RegisteredExternalOpener { + readonly extensionId: string; +} + +interface OpenersMemento { + [id: string]: RegisteredExternalOpener; +} + +/** + */ +export class ContributedExternalUriOpenersStore extends Disposable { + + private static readonly STORAGE_ID = 'externalUriOpeners'; + + private readonly _openers = new Map(); + private readonly _memento: Memento; + private _mementoObject: OpenersMemento; + + constructor( + @IStorageService storageService: IStorageService, + @IExtensionService private readonly _extensionService: IExtensionService + ) { + super(); + + this._memento = new Memento(ContributedExternalUriOpenersStore.STORAGE_ID, storageService); + this._mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + for (const id of Object.keys(this._mementoObject || {})) { + this.add(id, this._mementoObject[id].extensionId); + } + + this.invalidateOpenersForUninstalledExtension(); + + this._register(this._extensionService.onDidChangeExtensions(() => this.invalidateOpenersForUninstalledExtension())); + } + + public add(id: string, extensionId: string): void { + this._openers.set(id, { extensionId }); + + this._mementoObject[id] = { extensionId }; + this._memento.saveMemento(); + + this.updateSchema(); + } + + public delete(id: string): void { + this._openers.delete(id); + + delete this._mementoObject[id]; + this._memento.saveMemento(); + + this.updateSchema(); + } + + private async invalidateOpenersForUninstalledExtension() { + const registeredExtensions = await this._extensionService.getExtensions(); + for (const [id, entry] of this._openers) { + const isExtensionRegistered = registeredExtensions.some(r => r.identifier.value === entry.extensionId); + if (!isExtensionRegistered) { + this.delete(id); + } + } + } + + private updateSchema() { + const ids: string[] = []; + const descriptions: string[] = []; + + for (const [id, entry] of this._openers) { + ids.push(id); + descriptions.push(entry.extensionId); + } + + updateContributedOpeners(ids, descriptions); + } +} diff --git a/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpener.contribution.ts b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpener.contribution.ts new file mode 100644 index 000000000..34f349d2a --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpener.contribution.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { externalUriOpenersConfigurationNode } from 'vs/workbench/contrib/externalUriOpener/common/configuration'; +import { ExternalUriOpenerService, IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; + +registerSingleton(IExternalUriOpenerService, ExternalUriOpenerService); + +Registry.as(ConfigurationExtensions.Configuration) + .registerConfiguration(externalUriOpenersConfigurationNode); diff --git a/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts new file mode 100644 index 000000000..069ad5e48 --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts @@ -0,0 +1,216 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { firstOrDefault } from 'vs/base/common/arrays'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Iterable } from 'vs/base/common/iterator'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { LinkedList } from 'vs/base/common/linkedList'; +import { isWeb } from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; +import * as modes from 'vs/editor/common/modes'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IExternalOpener, IOpenerService } from 'vs/platform/opener/common/opener'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { defaultExternalUriOpenerId, ExternalUriOpenersConfiguration, externalUriOpenersSettingId } from 'vs/workbench/contrib/externalUriOpener/common/configuration'; +import { testUrlMatchesGlob } from 'vs/workbench/contrib/url/common/urlGlob'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; + + +export const IExternalUriOpenerService = createDecorator('externalUriOpenerService'); + + +export interface IExternalOpenerProvider { + getOpeners(targetUri: URI): AsyncIterable; +} + +export interface IExternalUriOpener { + readonly id: string; + readonly label: string; + + canOpen(uri: URI, token: CancellationToken): Promise; + openExternalUri(uri: URI, ctx: { sourceUri: URI }, token: CancellationToken): Promise; +} + +export interface IExternalUriOpenerService { + readonly _serviceBrand: undefined + + /** + * Registers a provider for external resources openers. + */ + registerExternalOpenerProvider(provider: IExternalOpenerProvider): IDisposable; +} + +export class ExternalUriOpenerService extends Disposable implements IExternalUriOpenerService, IExternalOpener { + + public readonly _serviceBrand: undefined; + + private readonly _providers = new LinkedList(); + + constructor( + @IOpenerService openerService: IOpenerService, + @IStorageService storageService: IStorageService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ILogService private readonly logService: ILogService, + @IPreferencesService private readonly preferencesService: IPreferencesService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + ) { + super(); + this._register(openerService.registerExternalOpener(this)); + } + + registerExternalOpenerProvider(provider: IExternalOpenerProvider): IDisposable { + const remove = this._providers.push(provider); + return { dispose: remove }; + } + + async openExternal(href: string, ctx: { sourceUri: URI, preferredOpenerId?: string }, token: CancellationToken): Promise { + + const targetUri = typeof href === 'string' ? URI.parse(href) : href; + + const allOpeners = await this.getAllOpenersForUri(targetUri); + + if (allOpeners.size === 0) { + return false; + } + + // First see if we have a preferredOpener + if (ctx.preferredOpenerId) { + if (ctx.preferredOpenerId === defaultExternalUriOpenerId) { + return false; + } + + const preferredOpener = allOpeners.get(ctx.preferredOpenerId); + if (preferredOpener) { + // Skip the `canOpen` check here since the opener was specifically requested. + return preferredOpener.openExternalUri(targetUri, ctx, token); + } + } + + // Check to see if we have a configured opener + const configuredOpener = this.getConfiguredOpenerForUri(allOpeners, targetUri); + if (configuredOpener) { + // Skip the `canOpen` check here since the opener was specifically requested. + return configuredOpener === defaultExternalUriOpenerId ? false : configuredOpener.openExternalUri(targetUri, ctx, token); + } + + // Then check to see if there is a valid opener + const validOpeners: Array<{ opener: IExternalUriOpener, priority: modes.ExternalUriOpenerPriority }> = []; + await Promise.all(Array.from(allOpeners.values()).map(async opener => { + let priority: modes.ExternalUriOpenerPriority; + try { + priority = await opener.canOpen(targetUri, token); + } catch (e) { + this.logService.error(e); + return; + } + + switch (priority) { + case modes.ExternalUriOpenerPriority.Option: + case modes.ExternalUriOpenerPriority.Default: + case modes.ExternalUriOpenerPriority.Preferred: + validOpeners.push({ opener, priority }); + break; + } + })); + + if (validOpeners.length === 0) { + return false; + } + + // See if we have a preferred opener first + const preferred = firstOrDefault(validOpeners.filter(x => x.priority === modes.ExternalUriOpenerPriority.Preferred)); + if (preferred) { + return preferred.opener.openExternalUri(targetUri, ctx, token); + } + + // See if we only have optional openers, use the default opener + if (validOpeners.every(x => x.priority === modes.ExternalUriOpenerPriority.Option)) { + return false; + } + + // Otherwise prompt + return this.showOpenerPrompt(validOpeners.map(x => x.opener), targetUri, ctx, token); + } + + private async getAllOpenersForUri(targetUri: URI): Promise> { + const allOpeners = new Map(); + await Promise.all(Iterable.map(this._providers, async (provider) => { + for await (const opener of provider.getOpeners(targetUri)) { + allOpeners.set(opener.id, opener); + } + })); + return allOpeners; + } + + private getConfiguredOpenerForUri(openers: Map, targetUri: URI): IExternalUriOpener | 'default' | undefined { + const config = this.configurationService.getValue(externalUriOpenersSettingId) || {}; + for (const [uriGlob, id] of Object.entries(config)) { + if (testUrlMatchesGlob(targetUri.toString(), uriGlob)) { + if (id === defaultExternalUriOpenerId) { + return 'default'; + } + + const entry = openers.get(id); + if (entry) { + return entry; + } + } + } + return undefined; + } + + private async showOpenerPrompt( + openers: ReadonlyArray, + targetUri: URI, + ctx: { sourceUri: URI }, + token: CancellationToken + ): Promise { + type PickItem = IQuickPickItem & { opener?: IExternalUriOpener | 'configureDefault' }; + + const items: Array = openers.map((opener): PickItem => { + return { + label: opener.label, + opener: opener + }; + }); + items.push( + { + label: isWeb + ? nls.localize('selectOpenerDefaultLabel.web', 'Open in new browser window') + : nls.localize('selectOpenerDefaultLabel', 'Open in default browser'), + opener: undefined + }, + { type: 'separator' }, + { + label: nls.localize('selectOpenerConfigureTitle', "Configure default opener..."), + opener: 'configureDefault' + }); + + const picked = await this.quickInputService.pick(items, { + placeHolder: nls.localize('selectOpenerPlaceHolder', "How would you like to open: {0}", targetUri.toString()) + }); + + if (!picked) { + // Still cancel the default opener here since we prompted the user + return true; + } + + if (typeof picked.opener === 'undefined') { + return false; // Fallback to default opener + } else if (picked.opener === 'configureDefault') { + await this.preferencesService.openGlobalSettings(true, { + revealSetting: { key: externalUriOpenersSettingId, edit: true } + }); + return true; + } else { + return picked.opener.openExternalUri(targetUri, ctx, token); + } + } +} diff --git a/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts b/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts new file mode 100644 index 000000000..557424f74 --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ExternalUriOpenerPriority } from 'vs/editor/common/modes'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IPickOptions, IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { ExternalUriOpenerService, IExternalOpenerProvider, IExternalUriOpener } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; + + +class MockQuickInputService implements Partial{ + + constructor( + private readonly pickIndex: number + ) { } + + public pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: true }, token?: CancellationToken): Promise; + public pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: false }, token?: CancellationToken): Promise; + public async pick(picks: Promise[]> | QuickPickInput[], options?: Omit, 'canPickMany'>, token?: CancellationToken): Promise { + const resolvedPicks = await picks; + const item = resolvedPicks[this.pickIndex]; + if (item.type === 'separator') { + return undefined; + } + return item; + } + +} + +suite('ExternalUriOpenerService', () => { + + let instantiationService: TestInstantiationService; + + setup(() => { + instantiationService = new TestInstantiationService(); + + instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IOpenerService, { + registerExternalOpener: () => { return Disposable.None; } + }); + }); + + test('Should not open if there are no openers', async () => { + const externalUriOpenerService: ExternalUriOpenerService = instantiationService.createInstance(ExternalUriOpenerService); + + externalUriOpenerService.registerExternalOpenerProvider(new class implements IExternalOpenerProvider { + async *getOpeners(_targetUri: URI): AsyncGenerator { + // noop + } + }); + + const uri = URI.parse('http://contoso.com'); + const didOpen = await externalUriOpenerService.openExternal(uri.toString(), { sourceUri: uri }, CancellationToken.None); + assert.strictEqual(didOpen, false); + }); + + test('Should prompt if there is at least one enabled opener', async () => { + instantiationService.stub(IQuickInputService, new MockQuickInputService(0)); + + const externalUriOpenerService: ExternalUriOpenerService = instantiationService.createInstance(ExternalUriOpenerService); + + let openedWithEnabled = false; + externalUriOpenerService.registerExternalOpenerProvider(new class implements IExternalOpenerProvider { + async *getOpeners(_targetUri: URI): AsyncGenerator { + yield { + id: 'disabled-id', + label: 'disabled', + canOpen: async () => ExternalUriOpenerPriority.None, + openExternalUri: async () => true, + }; + yield { + id: 'enabled-id', + label: 'enabled', + canOpen: async () => ExternalUriOpenerPriority.Default, + openExternalUri: async () => { + openedWithEnabled = true; + return true; + } + }; + } + }); + + const uri = URI.parse('http://contoso.com'); + const didOpen = await externalUriOpenerService.openExternal(uri.toString(), { sourceUri: uri }, CancellationToken.None); + assert.strictEqual(didOpen, true); + assert.strictEqual(openedWithEnabled, true); + }); + + test('Should automatically pick single preferred opener without prompt', async () => { + const externalUriOpenerService: ExternalUriOpenerService = instantiationService.createInstance(ExternalUriOpenerService); + + let openedWithPreferred = false; + externalUriOpenerService.registerExternalOpenerProvider(new class implements IExternalOpenerProvider { + async *getOpeners(_targetUri: URI): AsyncGenerator { + yield { + id: 'other-id', + label: 'other', + canOpen: async () => ExternalUriOpenerPriority.Default, + openExternalUri: async () => { + return true; + } + }; + yield { + id: 'preferred-id', + label: 'preferred', + canOpen: async () => ExternalUriOpenerPriority.Preferred, + openExternalUri: async () => { + openedWithPreferred = true; + return true; + } + }; + } + }); + + const uri = URI.parse('http://contoso.com'); + const didOpen = await externalUriOpenerService.openExternal(uri.toString(), { sourceUri: uri }, CancellationToken.None); + assert.strictEqual(didOpen, true); + assert.strictEqual(openedWithPreferred, true); + }); +}); diff --git a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index 7b93fdb20..1cc203a01 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -10,13 +10,11 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; /** * An implementation of editor for binary files that cannot be displayed. @@ -29,9 +27,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IOpenerService private readonly openerService: IOpenerService, - @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IQuickInputService private readonly quickInputService: IQuickInputService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, ) { @@ -55,7 +51,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { input.setForceOpenAsText(); // If more editors are installed that can handle this input, show a picker - await openEditorWith(input, undefined, options, this.group, this.editorService, this.configurationService, this.quickInputService); + await this.instantiationService.invokeFunction(openEditorWith, input, undefined, options, this.group); } } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index ec837dc48..cd7fb061e 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isFunction, assertIsDefined } from 'vs/base/common/types'; import { isValidBasename } from 'vs/base/common/extpath'; import { basename } from 'vs/base/common/resources'; -import { Action } from 'vs/base/common/actions'; +import { toAction } from 'vs/base/common/actions'; import { VIEWLET_ID, TEXT_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; @@ -97,7 +97,7 @@ export class TextFileEditor extends BaseTextEditor { } getTitle(): string { - return this.input ? this.input.getName() : nls.localize('textFileEditor', "Text File Editor"); + return this.input ? this.input.getName() : localize('textFileEditor', "Text File Editor"); } get input(): FileEditorInput | undefined { @@ -169,22 +169,24 @@ export class TextFileEditor extends BaseTextEditor { if ((error).fileOperationResult === FileOperationResult.FILE_IS_DIRECTORY) { this.openAsFolder(input); - throw new Error(nls.localize('openFolderError', "File is a directory")); + throw new Error(localize('openFolderError', "File is a directory")); } // Offer to create a file from the error if we have a file not found and the name is valid if ((error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && isValidBasename(basename(input.preferredResource))) { throw createErrorWithActions(toErrorMessage(error), { actions: [ - new Action('workbench.files.action.createMissingFile', nls.localize('createFile', "Create File"), undefined, true, async () => { - await this.textFileService.create(input.preferredResource); + toAction({ + id: 'workbench.files.action.createMissingFile', label: localize('createFile', "Create File"), run: async () => { + await this.textFileService.create([{ resource: input.preferredResource }]); - return this.editorService.openEditor({ - resource: input.preferredResource, - options: { - pinned: true // new file gets pinned by default - } - }); + return this.editorService.openEditor({ + resource: input.preferredResource, + options: { + pinned: true // new file gets pinned by default + } + }); + } }) ] }); diff --git a/src/vs/workbench/contrib/files/browser/explorerService.ts b/src/vs/workbench/contrib/files/browser/explorerService.ts index a1a537578..fa078a16d 100644 --- a/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -10,7 +10,7 @@ import { IFilesConfiguration, SortOrder } from 'vs/workbench/contrib/files/commo import { ExplorerItem, ExplorerModel } from 'vs/workbench/contrib/files/common/explorerModel'; import { URI } from 'vs/base/common/uri'; import { FileOperationEvent, FileOperation, IFileService, FileChangesEvent, FileChangeType, IResolveFileOptions } from 'vs/platform/files/common/files'; -import { dirname } from 'vs/base/common/resources'; +import { dirname, basename } from 'vs/base/common/resources'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -19,7 +19,7 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur import { IBulkEditService, ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { UndoRedoSource } from 'vs/platform/undoRedo/common/undoRedo'; import { IExplorerView, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IProgressService, ProgressLocation, IProgressNotificationOptions, IProgressCompositeOptions } from 'vs/platform/progress/common/progress'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -28,7 +28,7 @@ export const UNDO_REDO_SOURCE = new UndoRedoSource(); export class ExplorerService implements IExplorerService { declare readonly _serviceBrand: undefined; - private static readonly EXPLORER_FILE_CHANGES_REACT_DELAY = 1000; // delay in ms to react to file changes to give our internal events a chance to react first + private static readonly EXPLORER_FILE_CHANGES_REACT_DELAY = 500; // delay in ms to react to file changes to give our internal events a chance to react first private readonly disposables = new DisposableStore(); private editable: { stat: ExplorerItem, data: IEditableData } | undefined; @@ -60,17 +60,32 @@ export class ExplorerService implements IExplorerService { this.fileChangeEvents = []; // Filter to the ones we care - const types = [FileChangeType.ADDED, FileChangeType.DELETED]; + const types = [FileChangeType.DELETED]; if (this._sortOrder === SortOrder.Modified) { types.push(FileChangeType.UPDATED); } let shouldRefresh = false; + // For DELETED and UPDATED events go through the explorer model and check if any of the items got affected this.roots.forEach(r => { if (this.view && !shouldRefresh) { shouldRefresh = doesFileEventAffect(r, this.view, events, types); } }); + // For ADDED events we need to go through all the events and check if the explorer is already aware of some of them + // Or if they affect not yet resolved parts of the explorer. If that is the case we will not refresh. + events.forEach(e => { + if (!shouldRefresh) { + const added = e.getAdded(); + if (added.some(a => { + const parent = this.model.findClosest(dirname(a.resource)); + // Parent of the added resource is resolved and the explorer model is not aware of the added resource - we need to refresh + return parent && !parent.getChild(basename(a.resource)); + })) { + shouldRefresh = true; + } + } + }); if (shouldRefresh) { await this.refresh(false); @@ -95,7 +110,7 @@ export class ExplorerService implements IExplorerService { }); if (affected) { if (this.view) { - await this.view.refresh(true); + await this.view.setTreeInput(); } } })); @@ -125,19 +140,20 @@ export class ExplorerService implements IExplorerService { return this.view.getContext(respectMultiSelection); } - async applyBulkEdit(edit: ResourceFileEdit[], options: { undoLabel: string, progressLabel: string }): Promise { + async applyBulkEdit(edit: ResourceFileEdit[], options: { undoLabel: string, progressLabel: string, confirmBeforeUndo?: boolean, progressLocation?: ProgressLocation.Explorer | ProgressLocation.Window }): Promise { const cancellationTokenSource = new CancellationTokenSource(); - const promise = this.progressService.withProgress({ - location: ProgressLocation.Window, - delay: 500, + const promise = this.progressService.withProgress({ + location: options.progressLocation || ProgressLocation.Window, title: options.progressLabel, - cancellable: edit.length > 1 // Only allow cancellation when there is more than one edit. Since cancelling will not actually stop the current edit that is in progress. + cancellable: edit.length > 1, // Only allow cancellation when there is more than one edit. Since cancelling will not actually stop the current edit that is in progress. + delay: 500, }, async progress => { await this.bulkEditService.apply(edit, { undoRedoSource: UNDO_REDO_SOURCE, label: options.undoLabel, progress, - token: cancellationTokenSource.token + token: cancellationTokenSource.token, + confirmBeforeUndo: options.confirmBeforeUndo }); }, () => cancellationTokenSource.cancel()); await this.progressService.withProgress({ location: ProgressLocation.Explorer, delay: 500 }, () => promise); @@ -345,11 +361,11 @@ export class ExplorerService implements IExplorerService { } function doesFileEventAffect(item: ExplorerItem, view: IExplorerView, events: FileChangesEvent[], types: FileChangeType[]): boolean { - if (events.some(e => e.affects(item.resource, ...types))) { - return true; - } for (let [_name, child] of item.children) { if (view.isItemVisible(child)) { + if (events.some(e => e.contains(child.resource, ...types))) { + return true; + } if (child.isDirectory && child.isDirectoryResolved) { if (doesFileEventAffect(child, view, events, types)) { return true; diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 9745ec52d..4753cc33d 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -28,7 +28,8 @@ import { DelegatingEditorService } from 'vs/workbench/services/editor/browser/ed import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorPane } from 'vs/workbench/common/editor'; -import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { KeyChord, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; @@ -41,6 +42,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; const explorerViewIcon = registerIcon('explorer-view-icon', Codicon.files, localize('explorerViewIcon', 'View icon of the explorer view.')); +const openEditorsViewIcon = registerIcon('open-editors-view-icon', Codicon.book, localize('openEditorsIcon', 'View icon of the open editors view.')); export class ExplorerViewletViewsContribution extends Disposable implements IWorkbenchContribution { @@ -111,12 +113,13 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor id: OpenEditorsView.ID, name: OpenEditorsView.NAME, ctorDescriptor: new SyncDescriptor(OpenEditorsView), - containerIcon: explorerViewIcon, + containerIcon: openEditorsViewIcon, order: 0, when: OpenEditorsVisibleContext, canToggleVisibility: true, canMoveView: true, - collapsed: true, + collapsed: false, + hideByDefault: true, focusCommand: { id: 'workbench.files.action.focusOpenEditorsView', keybindings: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_E) } @@ -207,7 +210,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { let delay = 0; const config = this.configurationService.getValue(); - const delayEditorOpeningInOpenedEditors = !!config.workbench.editor.enablePreview; // No need to delay if preview is disabled + const delayEditorOpeningInOpenedEditors = !!config.workbench?.editor?.enablePreview; // No need to delay if preview is disabled const activeGroup = this.editorGroupService.activeGroup; if (delayEditorOpeningInOpenedEditors && group === activeGroup && !activeGroup.previewEditor) { diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 80deb8269..c9b2dcd2a 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -5,12 +5,12 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ToggleAutoSaveAction, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler, DOWNLOAD_LABEL, ShowOpenedFileInNewWindow } from 'vs/workbench/contrib/files/browser/fileActions'; +import { ToggleAutoSaveAction, FocusFilesExplorer, GlobalCompareResourcesAction, ShowActiveFileInExplorer, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler, DOWNLOAD_LABEL, ShowOpenedFileInNewWindow } from 'vs/workbench/contrib/files/browser/fileActions'; import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler'; import { SyncActionDescriptor, MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; -import { openWindowCommand, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, OpenEditorsDirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, newWindowCommand, OpenEditorsReadonlyEditorContext, OPEN_WITH_EXPLORER_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID, NEW_UNTITLED_FILE_LABEL } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { openWindowCommand, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, OpenEditorsDirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, newWindowCommand, OpenEditorsReadonlyEditorContext, OPEN_WITH_EXPLORER_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID, NEW_UNTITLED_FILE_LABEL, SAVE_ALL_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -36,12 +36,9 @@ import { Codicon } from 'vs/base/common/codicons'; const category = { value: nls.localize('filesCategory', "File"), original: 'File' }; const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(SaveAllAction, { primary: undefined, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_S }, win: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_S) } }), 'File: Save All', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(GlobalCompareResourcesAction), 'File: Compare Active File With...', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(FocusFilesExplorer), 'File: Focus on Files Explorer', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowActiveFileInExplorer), 'File: Reveal Active File in Side Bar', category.value); -registry.registerWorkbenchAction(SyncActionDescriptor.from(CollapseExplorerView), 'File: Collapse Folders in Explorer', category.value); -registry.registerWorkbenchAction(SyncActionDescriptor.from(RefreshExplorerView), 'File: Refresh Explorer', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(CompareWithClipboardAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_C) }), 'File: Compare Active File with Clipboard', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleAutoSaveAction), 'File: Toggle Auto Save', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowOpenedFileInNewWindow, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_O) }), 'File: Open Active File in New Window', category.value, EmptyWorkspaceSupportContext); @@ -612,7 +609,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '4_save', command: { - id: SaveAllAction.ID, + id: SAVE_ALL_COMMAND_ID, title: nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll"), precondition: DirtyWorkingCopiesContext }, diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 46aacd31f..69d3c400c 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -15,13 +15,12 @@ import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/com import { VIEWLET_ID, IFilesConfiguration, VIEW_ID } from 'vs/workbench/contrib/files/common/files'; import { ByteSize, IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; -import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel } from 'vs/editor/common/model'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_COMMAND_ID, SAVE_ALL_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_IN_GROUP_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -83,40 +82,6 @@ async function refreshIfSeparator(value: string, explorerService: IExplorerServi } } -/* New File */ -export class NewFileAction extends Action { - static readonly ID = 'workbench.files.action.createFileFromExplorer'; - static readonly LABEL = nls.localize('createNewFile', "New File"); - - constructor( - @ICommandService private commandService: ICommandService - ) { - super('explorer.newFile', NEW_FILE_LABEL); - this.class = 'explorer-action ' + Codicon.newFile.classNames; - } - - run(): Promise { - return this.commandService.executeCommand(NEW_FILE_COMMAND_ID); - } -} - -/* New Folder */ -export class NewFolderAction extends Action { - static readonly ID = 'workbench.files.action.createFolderFromExplorer'; - static readonly LABEL = nls.localize('createNewFolder', "New Folder"); - - constructor( - @ICommandService private commandService: ICommandService - ) { - super('explorer.newFolder', NEW_FOLDER_LABEL); - this.class = 'explorer-action ' + Codicon.newFolder.classNames; - } - - run(): Promise { - return this.commandService.executeCommand(NEW_FOLDER_COMMAND_ID); - } -} - async function deleteFiles(explorerService: IExplorerService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false): Promise { let primaryButton: string; if (useTrash) { @@ -168,6 +133,9 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer } let confirmation: IConfirmationResult; + // We do not support undo of folders, so in that case the delete action is irreversible + const deleteDetail = distinctElements.some(e => e.isDirectory) ? nls.localize('irreversible', "This action is irreversible!") : + distinctElements.length > 1 ? nls.localize('restorePlural', "You can restore these files using the Undo command") : nls.localize('restore', "You can restore this file using the Undo command"); // Check if we need to ask for confirmation at all if (skipConfirm || (useTrash && configurationService.getValue(CONFIRM_DELETE_SETTING_KEY) === false)) { @@ -199,7 +167,7 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer else { let { message, detail } = getDeleteMessage(distinctElements); detail += detail ? '\n' : ''; - detail += nls.localize('irreversible', "This action is irreversible!"); + detail += deleteDetail; confirmation = await dialogService.confirm({ message, detail, @@ -222,8 +190,8 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer try { const resourceFileEdits = distinctElements.map(e => new ResourceFileEdit(e.resource, undefined, { recursive: true, folder: e.isDirectory, skipTrashBin: !useTrash, maxSize: MAX_UNDO_FILE_SIZE })); const options = { - undoLabel: distinctElements.length > 1 ? nls.localize('deleteBulkEdit', "Delete {0} files", distinctElements.length) : nls.localize('deleteFileBulkEdit', "Delete {0}", distinctElements[0].name), - progressLabel: distinctElements.length > 1 ? nls.localize('deletingBulkEdit', "Deleting {0} files", distinctElements.length) : nls.localize('deletingFileBulkEdit', "Deleting {0}", distinctElements[0].name), + undoLabel: distinctElements.length > 1 ? nls.localize({ key: 'deleteBulkEdit', comment: ['Placeholder will be replaced by the number of files deleted'] }, "Delete {0} files", distinctElements.length) : nls.localize({ key: 'deleteFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file deleted'] }, "Delete {0}", distinctElements[0].name), + progressLabel: distinctElements.length > 1 ? nls.localize({ key: 'deletingBulkEdit', comment: ['Placeholder will be replaced by the number of files deleted'] }, "Deleting {0} files", distinctElements.length) : nls.localize({ key: 'deletingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file deleted'] }, "Deleting {0}", distinctElements[0].name), }; await explorerService.applyBulkEdit(resourceFileEdits, options); } catch (error) { @@ -234,7 +202,7 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer let primaryButton: string; if (useTrash) { errorMessage = isWindows ? nls.localize('binFailed', "Failed to delete using the Recycle Bin. Do you want to permanently delete instead?") : nls.localize('trashFailed', "Failed to delete using the Trash. Do you want to permanently delete instead?"); - detailMessage = nls.localize('irreversible', "This action is irreversible!"); + detailMessage = deleteDetail; primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently"); } else { errorMessage = toErrorMessage(error, false); @@ -411,6 +379,32 @@ export function incrementFileName(name: string, isFolder: boolean, incrementalNa return `${name.substr(0, lastIndexOfDot)}.1${name.substr(lastIndexOfDot)}`; } + // 123 => 124 + let noNameNoExtensionRegex = RegExp('(\\d+)$'); + if (!isFolder && lastIndexOfDot === -1 && name.match(noNameNoExtensionRegex)) { + return name.replace(noNameNoExtensionRegex, (match, g1?) => { + let number = parseInt(g1); + return number < maxNumber + ? String(number + 1).padStart(g1.length, '0') + : `${g1}.1`; + }); + } + + // file => file1 + // file1 => file2 + let noExtensionRegex = RegExp('(.*)(\d*)$'); + if (!isFolder && lastIndexOfDot === -1 && name.match(noExtensionRegex)) { + return name.replace(noExtensionRegex, (match, g1?, g2?) => { + let number = parseInt(g2); + if (isNaN(number)) { + number = 0; + } + return number < maxNumber + ? g1 + String(number + 1).padStart(g2.length, '0') + : `${g1}${g2}.1`; + }); + } + // folder.1=>folder.2 if (isFolder && name.match(/(\d+)$/)) { return name.replace(/(\d+)$/, (match, ...groups) => { @@ -560,20 +554,6 @@ export abstract class BaseSaveAllAction extends Action { } } -export class SaveAllAction extends BaseSaveAllAction { - - static readonly ID = 'workbench.action.files.saveAll'; - static readonly LABEL = SAVE_ALL_LABEL; - - get class(): string { - return 'explorer-action ' + Codicon.saveAll.classNames; - } - - protected doRun(): Promise { - return this.commandService.executeCommand(SAVE_ALL_COMMAND_ID); - } -} - export class SaveAllInGroupAction extends BaseSaveAllAction { static readonly ID = 'workbench.files.action.saveAllInGroup'; @@ -645,48 +625,6 @@ export class ShowActiveFileInExplorer extends Action { } } -export class CollapseExplorerView extends Action { - - static readonly ID = 'workbench.files.action.collapseExplorerFolders'; - static readonly LABEL = nls.localize('collapseExplorerFolders', "Collapse Folders in Explorer"); - - constructor(id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService, - @IExplorerService readonly explorerService: IExplorerService - ) { - super(id, label, 'explorer-action ' + Codicon.collapseAll.classNames); - } - - async run(): Promise { - const explorerViewlet = (await this.viewletService.openViewlet(VIEWLET_ID))?.getViewPaneContainer() as ExplorerViewPaneContainer; - const explorerView = explorerViewlet.getExplorerView(); - if (explorerView) { - explorerView.collapseAll(); - } - } -} - -export class RefreshExplorerView extends Action { - - static readonly ID = 'workbench.files.action.refreshFilesExplorer'; - static readonly LABEL = nls.localize('refreshExplorer', "Refresh Explorer"); - - - constructor( - id: string, label: string, - @IViewletService private readonly viewletService: IViewletService, - @IExplorerService private readonly explorerService: IExplorerService - ) { - super(id, label, 'explorer-action ' + Codicon.refresh.classNames); - } - - async run(): Promise { - await this.viewletService.openViewlet(VIEWLET_ID); - await this.explorerService.refresh(); - } -} - export class ShowOpenedFileInNewWindow extends Action { static readonly ID = 'workbench.action.files.showOpenedFileInNewWindow'; @@ -915,7 +853,8 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole const resourceToCreate = resources.joinPath(folder.resource, value); await explorerService.applyBulkEdit([new ResourceFileEdit(undefined, resourceToCreate, { folder: isFolder })], { undoLabel: nls.localize('createBulkEdit', "Create {0}", value), - progressLabel: nls.localize('creatingBulkEdit', "Creating {0}", value) + progressLabel: nls.localize('creatingBulkEdit', "Creating {0}", value), + confirmBeforeUndo: true }); await refreshIfSeparator(value, explorerService); @@ -1242,7 +1181,7 @@ const downloadFileHandler = async (accessor: ServicesAccessor) => { const destination = await fileDialogService.showSaveDialog({ availableFileSystems: [Schemas.file], saveLabel: mnemonicButtonLabel(nls.localize('downloadButton', "Download")), - title: explorerItem.isDirectory ? nls.localize('downloadFolder', "Download Folder") : nls.localize('downloadFile', "Download File"), + title: nls.localize('chooseWhereToDownload', "Choose Where to Download"), defaultUri }); @@ -1250,6 +1189,7 @@ const downloadFileHandler = async (accessor: ServicesAccessor) => { await explorerService.applyBulkEdit([new ResourceFileEdit(explorerItem.resource, destination, { overwrite: true, copy: true })], { undoLabel: nls.localize('downloadBulkEdit', "Download {0}", explorerItem.name), progressLabel: nls.localize('downloadingBulkEdit', "Downloading {0}", explorerItem.name), + progressLocation: ProgressLocation.Explorer }); } else { cts.cancel(); // User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100 @@ -1305,15 +1245,19 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { if (pasteShouldMove) { const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target)); const options = { - progressLabel: sourceTargetPairs.length > 1 ? nls.localize('movingBulkEdit', "Moving {0} files", sourceTargetPairs.length) : nls.localize('movingFileBulkEdit', "Moving {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), - undoLabel: sourceTargetPairs.length > 1 ? nls.localize('moveBulkEdit', "Move {0} files", sourceTargetPairs.length) : nls.localize('moveFileBulkEdit', "Move {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) + progressLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'movingBulkEdit', comment: ['Placeholder will be replaced by the number of files being moved'] }, "Moving {0} files", sourceTargetPairs.length) + : nls.localize({ key: 'movingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Moving {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), + undoLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'moveBulkEdit', comment: ['Placeholder will be replaced by the number of files being moved'] }, "Move {0} files", sourceTargetPairs.length) + : nls.localize({ key: 'moveFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Move {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) }; await explorerService.applyBulkEdit(resourceFileEdits, options); } else { const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { copy: true })); const options = { - progressLabel: sourceTargetPairs.length > 1 ? nls.localize('copyingBulkEdit', "Copying {0} files", sourceTargetPairs.length) : nls.localize('copyingFileBulkEdit', "Copying {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), - undoLabel: sourceTargetPairs.length > 1 ? nls.localize('copyBulkEdit', "Copy {0} files", sourceTargetPairs.length) : nls.localize('copyFileBulkEdit', "Copy {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) + progressLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'copyingBulkEdit', comment: ['Placeholder will be replaced by the number of files being copied'] }, "Copying {0} files", sourceTargetPairs.length) + : nls.localize({ key: 'copyingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Copying {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), + undoLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'copyBulkEdit', comment: ['Placeholder will be replaced by the number of files being copied'] }, "Copy {0} files", sourceTargetPairs.length) + : nls.localize({ key: 'copyFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Copy {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) }; await explorerService.applyBulkEdit(resourceFileEdits, options); } diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 8e1badc5f..ef6eb4dd9 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -40,8 +40,6 @@ import { coalesce } from 'vs/base/common/arrays'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith'; import { isPromiseCanceledError } from 'vs/base/common/errors'; @@ -356,13 +354,11 @@ CommandsRegistry.registerCommand({ handler: async (accessor, resource: URI | object) => { const editorService = accessor.get(IEditorService); const editorGroupsService = accessor.get(IEditorGroupsService); - const configurationService = accessor.get(IConfigurationService); - const quickInputService = accessor.get(IQuickInputService); const uri = getResourceForCommand(resource, accessor.get(IListService), accessor.get(IEditorService)); if (uri) { const input = editorService.createEditorInput({ resource: uri }); - openEditorWith(input, undefined, undefined, editorGroupsService.activeGroup, editorService, configurationService, quickInputService); + openEditorWith(accessor, input, undefined, undefined, editorGroupsService.activeGroup); } } }); @@ -482,7 +478,12 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -CommandsRegistry.registerCommand({ +KeybindingsRegistry.registerCommandAndKeybindingRule({ + when: undefined, + weight: KeybindingWeight.WorkbenchContrib, + primary: undefined, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_S }, + win: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_S) }, id: SAVE_ALL_COMMAND_ID, handler: (accessor) => { return saveDirtyEditorsOfGroups(accessor, accessor.get(IEditorGroupsService).getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE), { reason: SaveReason.EXPLICIT }); @@ -664,12 +665,10 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ if (typeof args?.viewType === 'string') { const editorGroupsService = accessor.get(IEditorGroupsService); - const configurationService = accessor.get(IConfigurationService); - const quickInputService = accessor.get(IQuickInputService); const textInput = editorService.createEditorInput({ options: { pinned: true } }); const group = editorGroupsService.activeGroup; - await openEditorWith(textInput, args.viewType, { pinned: true }, group, editorService, configurationService, quickInputService); + await openEditorWith(accessor, textInput, args.viewType, { pinned: true }, group); } else { await editorService.openEditor({ options: { pinned: true } }); // untitled are always pinned } diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 7ea77d336..f4b223bf4 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -198,8 +198,8 @@ const hotExitConfiguration: IConfigurationPropertySchema = platform.isNative ? 'default': HotExitConfiguration.ON_EXIT, 'markdownEnumDescriptions': [ nls.localize('hotExit.off', 'Disable hot exit. A prompt will show when attempting to close a window with dirty files.'), - nls.localize('hotExit.onExit', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu). All windows without folders opened will be restored upon next launch. A list of workspaces with unsaved files can be accessed via `File > Open Recent > More...`'), - nls.localize('hotExit.onExitAndWindowClose', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), and also for any window with a folder opened regardless of whether it\'s the last window. All windows without folders opened will be restored upon next launch. A list of workspaces with unsaved files can be accessed via `File > Open Recent > More...`') + nls.localize('hotExit.onExit', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu). All windows without folders opened will be restored upon next launch. A list of previously opened windows with unsaved files can be accessed via `File > Open Recent > More...`'), + nls.localize('hotExit.onExitAndWindowClose', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), and also for any window with a folder opened regardless of whether it\'s the last window. All windows without folders opened will be restored upon next launch. A list of previously opened windows with unsaved files can be accessed via `File > Open Recent > More...`') ], 'description': nls.localize('hotExit', "Controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) } : { @@ -386,7 +386,7 @@ configurationRegistry.registerConfiguration({ nls.localize({ key: 'everything', comment: ['This is the description of an option'] }, "Format the whole file."), nls.localize({ key: 'modification', comment: ['This is the description of an option'] }, "Format modifications (requires source control)."), ], - 'markdownDescription': nls.localize('formatOnSaveMode', "Controls if format on save formats the whole file or only modifications. Only applies when `#editor.formatOnSave#` is `true`."), + 'markdownDescription': nls.localize('formatOnSaveMode', "Controls if format on save formats the whole file or only modifications. Only applies when `#editor.formatOnSave#` is enabled."), 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE, }, } @@ -492,7 +492,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { UndoCommand.addImplementation(110, (accessor: ServicesAccessor) => { const undoRedoService = accessor.get(IUndoRedoService); const explorerService = accessor.get(IExplorerService); - if (explorerService.hasViewFocus()) { + if (explorerService.hasViewFocus() && undoRedoService.canUndo(UNDO_REDO_SOURCE)) { undoRedoService.undo(UNDO_REDO_SOURCE); return true; } @@ -503,7 +503,7 @@ UndoCommand.addImplementation(110, (accessor: ServicesAccessor) => { RedoCommand.addImplementation(110, (accessor: ServicesAccessor) => { const undoRedoService = accessor.get(IUndoRedoService); const explorerService = accessor.get(IExplorerService); - if (explorerService.hasViewFocus()) { + if (explorerService.hasViewFocus() && undoRedoService.canRedo(UNDO_REDO_SOURCE)) { undoRedoService.redo(UNDO_REDO_SOURCE); return true; } diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index 95e137cb3..bd0880343 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -16,6 +16,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IEditableData } from 'vs/workbench/common/views'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; +import { ProgressLocation } from 'vs/platform/progress/common/progress'; export interface IExplorerService { @@ -34,7 +35,7 @@ export interface IExplorerService { refresh(): Promise; setToCopy(stats: ExplorerItem[], cut: boolean): Promise; isCut(stat: ExplorerItem): boolean; - applyBulkEdit(edit: ResourceFileEdit[], options: { undoLabel: string, progressLabel: string }): Promise; + applyBulkEdit(edit: ResourceFileEdit[], options: { undoLabel: string, progressLabel: string, confirmBeforeUndo?: boolean, progressLocation?: ProgressLocation.Explorer | ProgressLocation.Window }): Promise; /** * Selects and reveal the file element provided by the given resource if its found in the explorer. diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index 8d645ccaa..29bad5473 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -89,7 +89,7 @@ height: 22px; } -.monaco-workbench .explorer-viewlet .explorer-item .monaco-inputbox > .wrapper > .input { +.monaco-workbench .explorer-viewlet .explorer-item .monaco-inputbox > .ibwrapper > .input { padding: 0; height: 20px; } diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index d649859fc..7e70f008f 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -11,7 +11,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { ResourcesDropHandler, DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { listDropBackground } from 'vs/platform/theme/common/colorRegistry'; import { ILabelService } from 'vs/platform/label/common/label'; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index a9309b467..59ca8405e 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -8,30 +8,30 @@ import { URI } from 'vs/base/common/uri'; import * as perf from 'vs/base/common/performance'; import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; -import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext } from 'vs/workbench/contrib/files/common/files'; -import { NewFolderAction, NewFileAction, FileCopiedContext, RefreshExplorerView, CollapseExplorerView } from 'vs/workbench/contrib/files/browser/fileActions'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext, VIEW_ID, VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; +import { FileCopiedContext, NEW_FILE_COMMAND_ID, NEW_FOLDER_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileActions'; import * as DOM from 'vs/base/browser/dom'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { ExplorerDecorationsProvider } from 'vs/workbench/contrib/files/browser/views/explorerDecorationsProvider'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; import { WorkbenchCompressibleAsyncDataTree } from 'vs/platform/list/browser/listService'; import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IViewPaneOptions, ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ILabelService } from 'vs/platform/label/common/label'; import { ExplorerDelegate, ExplorerDataSource, FilesRenderer, ICompressedNavigationController, FilesFilter, FileSorter, FileDragAndDrop, ExplorerCompressionDelegate, isCompressedFolderName } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeContextMenuEvent, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; -import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, IMenu, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; @@ -52,6 +52,9 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { Codicon } from 'vs/base/common/codicons'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; interface IExplorerViewColors extends IColorMapping { listDropBackground?: ColorValue | undefined; @@ -75,6 +78,16 @@ function hasExpandedRootChild(tree: WorkbenchCompressibleAsyncDataTree { + if (stat instanceof NewExplorerItem) { + return `new:${stat.resource}`; + } + + return stat.resource; + } +}; + export function getContext(focus: ExplorerItem[], selection: ExplorerItem[], respectMultiSelection: boolean, compressedNavigationControllerProvider: { getCompressedNavigationController(stat: ExplorerItem): ICompressedNavigationController | undefined }): ExplorerItem[] { @@ -146,7 +159,6 @@ export class ExplorerView extends ViewPane { private shouldRefresh = true; private dragHandler!: DelayedDragHandler; private autoReveal: boolean | 'focusNoScroll' = false; - private actions: IAction[] | undefined; private decorationsProvider: ExplorerDecorationsProvider | undefined; constructor( @@ -284,19 +296,6 @@ export class ExplorerView extends ViewPane { })); } - getActions(): IAction[] { - if (!this.actions) { - this.actions = [ - this.instantiationService.createInstance(NewFileAction), - this.instantiationService.createInstance(NewFolderAction), - this.instantiationService.createInstance(RefreshExplorerView, RefreshExplorerView.ID, RefreshExplorerView.LABEL), - this.instantiationService.createInstance(CollapseExplorerView, CollapseExplorerView.ID, CollapseExplorerView.LABEL) - ]; - this.actions.forEach(a => this._register(a)); - } - return this.actions; - } - focus(): void { this.tree.domFocus(); @@ -381,15 +380,7 @@ export class ExplorerView extends ViewPane { this.instantiationService.createInstance(ExplorerDataSource), { compressionEnabled: isCompressionEnabled(), accessibilityProvider: this.renderer, - identityProvider: { - getId: (stat: ExplorerItem) => { - if (stat instanceof NewExplorerItem) { - return `new:${stat.resource}`; - } - - return stat.resource; - } - }, + identityProvider, keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (stat: ExplorerItem) => { if (this.explorerService.isEditable(stat)) { @@ -593,7 +584,9 @@ export class ExplorerView extends ViewPane { } const toRefresh = item || this.tree.getInput(); - return this.tree.updateChildren(toRefresh, recursive); + return this.tree.updateChildren(toRefresh, recursive, false, { + diffIdentityProvider: identityProvider + }); } focusNeighbourIfItemFocused(item: ExplorerItem): void { @@ -635,7 +628,7 @@ export class ExplorerView extends ViewPane { const initialInputSetup = !this.tree.getInput(); if (initialInputSetup) { - perf.mark('willResolveExplorer'); + perf.mark('code/willResolveExplorer'); } const roots = this.explorerService.roots; let input: ExplorerItem | ExplorerItem[] = roots[0]; @@ -675,7 +668,7 @@ export class ExplorerView extends ViewPane { } } if (initialInputSetup) { - perf.mark('didResolveExplorer'); + perf.mark('code/didResolveExplorer'); } }); @@ -856,3 +849,93 @@ function createFileIconThemableTreeContainerScope(container: HTMLElement, themeS onDidChangeFileIconTheme(themeService.getFileIconTheme()); return themeService.onDidFileIconThemeChange(onDidChangeFileIconTheme); } + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.files.action.createFileFromExplorer', + title: nls.localize('createNewFile', "New File"), + f1: false, + icon: Codicon.newFile, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VIEW_ID), + order: 10 + } + }); + } + + run(accessor: ServicesAccessor): void { + const commandService = accessor.get(ICommandService); + commandService.executeCommand(NEW_FILE_COMMAND_ID); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.files.action.createFolderFromExplorer', + title: nls.localize('createNewFolder', "New Folder"), + f1: false, + icon: Codicon.newFolder, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VIEW_ID), + order: 20 + } + }); + } + + run(accessor: ServicesAccessor): void { + const commandService = accessor.get(ICommandService); + commandService.executeCommand(NEW_FOLDER_COMMAND_ID); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.files.action.refreshFilesExplorer', + title: nls.localize('refreshExplorer', "Refresh Explorer"), + f1: true, + icon: Codicon.refresh, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VIEW_ID), + order: 30 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const viewletService = accessor.get(IViewletService); + const explorerService = accessor.get(IExplorerService); + await viewletService.openViewlet(VIEWLET_ID); + await explorerService.refresh(); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'workbench.files.action.collapseExplorerFolders', + title: nls.localize('collapseExplorerFolders', "Collapse Folders in Explorer"), + viewId: VIEW_ID, + f1: true, + icon: Codicon.collapseAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VIEW_ID), + order: 40 + } + }); + } + + runInView(_accessor: ServicesAccessor, view: ExplorerView): void { + view.collapseAll(); + } +}); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 9e8330b3d..6bf781d2e 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -451,7 +451,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer { if (e.equals(KeyCode.Enter)) { - if (inputBox.validate()) { + if (!inputBox.validate()) { done(true, true); } } else if (e.equals(KeyCode.Escape)) { @@ -626,7 +626,7 @@ export class FilesFilter implements ITreeFilter { stat.isExcluded = true; return false; } - if (this.explorerService.getEditableData(stat) || stat.isRoot) { + if (this.explorerService.getEditableData(stat)) { return true; // always visible } diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 3535108c3..db0b429cf 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -9,16 +9,15 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { IAction, ActionRunner, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorGroupsService, IEditorGroup, GroupChangeKind, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorGroupsService, IEditorGroup, GroupChangeKind, GroupsOrder, GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IEditorInput, Verbosity, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; -import { SaveAllAction, SaveAllInGroupAction, CloseGroupAction } from 'vs/workbench/contrib/files/browser/fileActions'; +import { SaveAllInGroupAction, CloseGroupAction } from 'vs/workbench/contrib/files/browser/fileActions'; import { OpenEditorsFocusedContext, ExplorerFocusedContext, IFilesConfiguration, OpenEditor } from 'vs/workbench/contrib/files/common/files'; import { CloseAllEditorsAction, CloseEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; -import { ToggleEditorLayoutAction } from 'vs/workbench/browser/actions/layoutActions'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; @@ -30,11 +29,11 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; -import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { IMenuService, MenuId, IMenu, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { ResourcesDropHandler, fillResourceDataTransfers, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; import { memoize } from 'vs/base/common/decorators'; @@ -49,6 +48,10 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { compareFileNamesDefault } from 'vs/base/common/comparers'; +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ICommandService } from 'vs/platform/commands/common/commands'; const $ = dom.$; @@ -92,14 +95,26 @@ export class OpenEditorsView extends ViewPane { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.structuralRefreshDelay = 0; + let labelChangeListeners: IDisposable[] = []; this.listRefreshScheduler = new RunOnceScheduler(() => { + labelChangeListeners = dispose(labelChangeListeners); const previousLength = this.list.length; - this.list.splice(0, this.list.length, this.getElements()); + const elements = this.getElements(); + this.list.splice(0, this.list.length, elements); this.focusActiveEditor(); if (previousLength !== this.list.length) { this.updateSize(); } this.needsRefresh = false; + + if (this.sortOrder === 'alphabetical') { + // We need to resort the list if the editor label changed + elements.forEach(e => { + if (e instanceof OpenEditor) { + labelChangeListeners.push(e.editor.onDidChangeLabel(() => this.listRefreshScheduler.schedule())); + } + }); + } }, this.structuralRefreshDelay); this.sortOrder = configurationService.getValue('explorer.openEditors.sortOrder'); @@ -151,6 +166,7 @@ export class OpenEditorsView extends ViewPane { case GroupChangeKind.EDITOR_STICKY: case GroupChangeKind.EDITOR_PIN: { this.list.splice(index, 1, [new OpenEditor(e.editor!, group)]); + this.focusActiveEditor(); break; } case GroupChangeKind.EDITOR_OPEN: @@ -294,14 +310,6 @@ export class OpenEditorsView extends ViewPane { })); } - getActions(): IAction[] { - return [ - this.instantiationService.createInstance(ToggleEditorLayoutAction, ToggleEditorLayoutAction.ID, ToggleEditorLayoutAction.LABEL), - this.instantiationService.createInstance(SaveAllAction, SaveAllAction.ID, SaveAllAction.LABEL), - this.instantiationService.createInstance(CloseAllEditorsAction, CloseAllEditorsAction.ID, CloseAllEditorsAction.LABEL) - ]; - } - focus(): void { super.focus(); this.list.domFocus(); @@ -705,3 +713,86 @@ class OpenEditorsAccessibilityProvider implements IListAccessibilityProvider { + const editorGroupService = accessor.get(IEditorGroupsService); + const newOrientation = (editorGroupService.orientation === GroupOrientation.VERTICAL) ? GroupOrientation.HORIZONTAL : GroupOrientation.VERTICAL; + editorGroupService.setGroupOrientation(newOrientation); + } +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: 'z_flip', + command: { + id: toggleEditorGroupLayoutId, + title: nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Flip &&Layout") + }, + order: 1 +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.files.saveAll', + title: SAVE_ALL_LABEL, + f1: true, + icon: Codicon.saveAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', OpenEditorsView.ID), + order: 20 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand(SAVE_ALL_COMMAND_ID); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'openEditors.closeAll', + title: CloseAllEditorsAction.LABEL, + f1: false, + icon: Codicon.closeAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', OpenEditorsView.ID), + order: 30 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const instantiationService = accessor.get(IInstantiationService); + const closeAll = instantiationService.createInstance(CloseAllEditorsAction, CloseAllEditorsAction.ID, CloseAllEditorsAction.LABEL); + await closeAll.run(); + } +}); diff --git a/src/vs/workbench/contrib/files/common/workspaceWatcher.ts b/src/vs/workbench/contrib/files/common/workspaceWatcher.ts index 04bfd96da..1534bdc86 100644 --- a/src/vs/workbench/contrib/files/common/workspaceWatcher.ts +++ b/src/vs/workbench/contrib/files/common/workspaceWatcher.ts @@ -89,7 +89,7 @@ export class WorkspaceWatcher extends Disposable { if (msg.indexOf('ENOSPC') >= 0) { this.notificationService.prompt( Severity.Warning, - localize('enospcError', "Unable to watch for file changes in this large workspace. Please follow the instructions link to resolve this issue."), + localize('enospcError', "Unable to watch for file changes in this large workspace folder. Please follow the instructions link to resolve this issue."), [{ label: localize('learnMore', "Instructions"), run: () => this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=867693')) diff --git a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts index d310e9872..145efa742 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { EditorOptions } from 'vs/workbench/common/editor'; import { FileOperationError, FileOperationResult, IFileService, MIN_MAX_MEMORY_SIZE_MB, FALLBACK_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/common/files'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; -import { Action } from 'vs/base/common/actions'; +import { toAction } from 'vs/base/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -56,18 +56,22 @@ export class NativeTextFileEditor extends TextFileEditor { if ((error).fileOperationResult === FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT) { const memoryLimit = Math.max(MIN_MAX_MEMORY_SIZE_MB, +this.textResourceConfigurationService.getValue(undefined, 'files.maxMemoryForLargeFilesMB') || FALLBACK_MAX_MEMORY_SIZE_MB); - throw createErrorWithActions(nls.localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), { + throw createErrorWithActions(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), { actions: [ - new Action('workbench.window.action.relaunchWithIncreasedMemoryLimit', nls.localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), undefined, true, () => { - return this.nativeHostService.relaunch({ - addArgs: [ - `--max-memory=${memoryLimit}` - ] - }); + toAction({ + id: 'workbench.window.action.relaunchWithIncreasedMemoryLimit', label: localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), run: () => { + return this.nativeHostService.relaunch({ + addArgs: [ + `--max-memory=${memoryLimit}` + ] + }); + } + }), + toAction({ + id: 'workbench.window.action.configureMemoryLimit', label: localize('configureMemoryLimit', 'Configure Memory Limit'), run: () => { + return this.preferencesService.openGlobalSettings(undefined, { query: 'files.maxMemoryForLargeFilesMB' }); + } }), - new Action('workbench.window.action.configureMemoryLimit', nls.localize('configureMemoryLimit', 'Configure Memory Limit'), undefined, true, () => { - return this.preferencesService.openGlobalSettings(undefined, { query: 'files.maxMemoryForLargeFilesMB' }); - }) ] }); } diff --git a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index 644cdf743..821be9888 100644 --- a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -119,8 +119,6 @@ suite('EditorAutoSave', () => { }); function awaitModelSaved(model: ITextFileEditorModel): Promise { - return new Promise(c => { - Event.once(model.onDidChangeDirty)(c); - }); + return Event.toPromise(Event.once(model.onDidChangeDirty)); } }); diff --git a/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts b/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts index bf0642cde..bfa18a3b7 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts @@ -258,7 +258,7 @@ suite('Files - Increment file name smart', () => { assert.strictEqual(result, '2-test.js'); }); - test('Increment file name with prefix version with `-` as separator', function () { + test('Increment file name with prefix version with `_` as separator', function () { const name = '1_test.js'; const result = incrementFileName(name, false, 'smart'); assert.strictEqual(result, '2_test.js'); @@ -270,6 +270,36 @@ suite('Files - Increment file name smart', () => { assert.strictEqual(result, '9007199254740992.test.1.js'); }); + test('Increment file name with just version and no extension', function () { + const name = '001004'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, '001005'); + }); + + test('Increment file name with just version and no extension, too big number', function () { + const name = '9007199254740992'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, '9007199254740992.1'); + }); + + test('Increment file name with no extension and no version', function () { + const name = 'file'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, 'file1'); + }); + + test('Increment file name with no extension', function () { + const name = 'file1'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, 'file2'); + }); + + test('Increment file name with no extension, too big number', function () { + const name = 'file9007199254740992'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, 'file9007199254740992.1'); + }); + test('Increment folder name with prefix version', function () { const name = '1.test'; const result = incrementFileName(name, true, 'smart'); diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 0df2ed5d5..bb2a00dda 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -102,28 +102,28 @@ suite('Files - FileEditorInput', () => { const preferredResource = toResource.call(this, '/foo/bar/UPDATEFILE.js'); const inputWithoutPreferredResource = createFileInput(resource); - assert.equal(inputWithoutPreferredResource.resource.toString(), resource.toString()); - assert.equal(inputWithoutPreferredResource.preferredResource.toString(), resource.toString()); + assert.strictEqual(inputWithoutPreferredResource.resource.toString(), resource.toString()); + assert.strictEqual(inputWithoutPreferredResource.preferredResource.toString(), resource.toString()); const inputWithPreferredResource = createFileInput(resource, preferredResource); - assert.equal(inputWithPreferredResource.resource.toString(), resource.toString()); - assert.equal(inputWithPreferredResource.preferredResource.toString(), preferredResource.toString()); + assert.strictEqual(inputWithPreferredResource.resource.toString(), resource.toString()); + assert.strictEqual(inputWithPreferredResource.preferredResource.toString(), preferredResource.toString()); let didChangeLabel = false; const listener = inputWithPreferredResource.onDidChangeLabel(e => { didChangeLabel = true; }); - assert.equal(inputWithPreferredResource.getName(), 'UPDATEFILE.js'); + assert.strictEqual(inputWithPreferredResource.getName(), 'UPDATEFILE.js'); const otherPreferredResource = toResource.call(this, '/FOO/BAR/updateFILE.js'); inputWithPreferredResource.setPreferredResource(otherPreferredResource); - assert.equal(inputWithPreferredResource.resource.toString(), resource.toString()); - assert.equal(inputWithPreferredResource.preferredResource.toString(), otherPreferredResource.toString()); - assert.equal(inputWithPreferredResource.getName(), 'updateFILE.js'); - assert.equal(didChangeLabel, true); + assert.strictEqual(inputWithPreferredResource.resource.toString(), resource.toString()); + assert.strictEqual(inputWithPreferredResource.preferredResource.toString(), otherPreferredResource.toString()); + assert.strictEqual(inputWithPreferredResource.getName(), 'updateFILE.js'); + assert.strictEqual(didChangeLabel, true); listener.dispose(); }); @@ -135,20 +135,20 @@ suite('Files - FileEditorInput', () => { }); const input = createFileInput(toResource.call(this, '/foo/bar/file.js'), undefined, mode); - assert.equal(input.getPreferredMode(), mode); + assert.strictEqual(input.getPreferredMode(), mode); const model = await input.resolve() as TextFileEditorModel; - assert.equal(model.textEditorModel!.getModeId(), mode); + assert.strictEqual(model.textEditorModel!.getModeId(), mode); input.setMode('text'); - assert.equal(input.getPreferredMode(), 'text'); - assert.equal(model.textEditorModel!.getModeId(), PLAINTEXT_MODE_ID); + assert.strictEqual(input.getPreferredMode(), 'text'); + assert.strictEqual(model.textEditorModel!.getModeId(), PLAINTEXT_MODE_ID); const input2 = createFileInput(toResource.call(this, '/foo/bar/file.js')); input2.setPreferredMode(mode); const model2 = await input2.resolve() as TextFileEditorModel; - assert.equal(model2.textEditorModel!.getModeId(), mode); + assert.strictEqual(model2.textEditorModel!.getModeId(), mode); }); test('matches', function () { @@ -169,10 +169,10 @@ suite('Files - FileEditorInput', () => { const input = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); input.setEncoding('utf16', EncodingMode.Encode); - assert.equal(input.getEncoding(), 'utf16'); + assert.strictEqual(input.getEncoding(), 'utf16'); const resolved = await input.resolve() as TextFileEditorModel; - assert.equal(input.getEncoding(), resolved.getEncoding()); + assert.strictEqual(input.getEncoding(), resolved.getEncoding()); resolved.dispose(); }); @@ -237,7 +237,7 @@ suite('Files - FileEditorInput', () => { const model = await accessor.textFileService.files.resolve(input.resource); model.textEditorModel?.setValue('hello world'); - assert.equal(listenerCount, 1); + assert.strictEqual(listenerCount, 1); assert.ok(input.isDirty()); input.dispose(); @@ -269,7 +269,7 @@ suite('Files - FileEditorInput', () => { assert.fail('File Editor Input Factory missing'); } - assert.equal(factory.canSerialize(input), true); + assert.strictEqual(factory.canSerialize(input), true); const inputSerialized = factory.serialize(input); if (!inputSerialized) { @@ -277,7 +277,7 @@ suite('Files - FileEditorInput', () => { } const inputDeserialized = factory.deserialize(instantiationService, inputSerialized); - assert.equal(input.matches(inputDeserialized), true); + assert.strictEqual(input.matches(inputDeserialized), true); const preferredResource = toResource.call(this, '/foo/bar/UPDATEfile.js'); const inputWithPreferredResource = createFileInput(toResource.call(this, '/foo/bar/updatefile.js'), preferredResource); @@ -288,8 +288,8 @@ suite('Files - FileEditorInput', () => { } const inputWithPreferredResourceDeserialized = factory.deserialize(instantiationService, inputWithPreferredResourceSerialized) as FileEditorInput; - assert.equal(inputWithPreferredResource.resource.toString(), inputWithPreferredResourceDeserialized.resource.toString()); - assert.equal(inputWithPreferredResource.preferredResource.toString(), inputWithPreferredResourceDeserialized.preferredResource.toString()); + assert.strictEqual(inputWithPreferredResource.resource.toString(), inputWithPreferredResourceDeserialized.resource.toString()); + assert.strictEqual(inputWithPreferredResource.preferredResource.toString(), inputWithPreferredResourceDeserialized.preferredResource.toString()); }); test('preferred name/description', async function () { @@ -302,16 +302,16 @@ suite('Files - FileEditorInput', () => { didChangeLabelCounter++; }); - assert.equal(customFileInput.getName(), 'My Name'); - assert.equal(customFileInput.getDescription(), 'My Description'); + assert.strictEqual(customFileInput.getName(), 'My Name'); + assert.strictEqual(customFileInput.getDescription(), 'My Description'); customFileInput.setPreferredName('My Name 2'); customFileInput.setPreferredDescription('My Description 2'); - assert.equal(customFileInput.getName(), 'My Name 2'); - assert.equal(customFileInput.getDescription(), 'My Description 2'); + assert.strictEqual(customFileInput.getName(), 'My Name 2'); + assert.strictEqual(customFileInput.getDescription(), 'My Description 2'); - assert.equal(didChangeLabelCounter, 2); + assert.strictEqual(didChangeLabelCounter, 2); customFileInput.dispose(); @@ -332,7 +332,7 @@ suite('Files - FileEditorInput', () => { assert.notEqual(fileInput.getName(), 'My Name 2'); assert.notEqual(fileInput.getDescription(), 'My Description 2'); - assert.equal(didChangeLabelCounter, 0); + assert.strictEqual(didChangeLabelCounter, 0); fileInput.dispose(); }); diff --git a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts index 70c2fa9e2..00b36384a 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts @@ -27,8 +27,8 @@ suite('Files - FileOnDiskContentProvider', () => { const content = await provider.provideTextContent(uri.with({ scheme: 'conflictResolution', query: JSON.stringify({ scheme: uri.scheme }) })); assert.ok(content); - assert.equal(snapshotToString(content!.createSnapshot()), 'Hello Html'); - assert.equal(accessor.fileService.getLastReadFileUri().scheme, uri.scheme); - assert.equal(accessor.fileService.getLastReadFileUri().path, uri.path); + assert.strictEqual(snapshotToString(content!.createSnapshot()), 'Hello Html'); + assert.strictEqual(accessor.fileService.getLastReadFileUri().scheme, uri.scheme); + assert.strictEqual(accessor.fileService.getLastReadFileUri().path, uri.path); }); }); diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts index 9b346315b..33342c8fa 100644 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts @@ -27,6 +27,7 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { raceTimeout } from 'vs/base/common/async'; suite('Files - TextFileEditor', () => { @@ -73,7 +74,7 @@ suite('Files - TextFileEditor', () => { const accessor = instantiationService.createInstance(TestServiceAccessor); - await part.whenRestored; + await raceTimeout(part.whenRestored, 2000, () => assert.fail('textFileEditor.test.ts: Unexpected long time to wait for part to restore (#112649)')); return [part, accessor, instantiationService, editorService]; } @@ -86,7 +87,7 @@ suite('Files - TextFileEditor', () => { return viewStateTest(this, false); }); - async function viewStateTest(context: Mocha.ITestCallbackContext, restoreViewState: boolean): Promise { + async function viewStateTest(context: Mocha.Context, restoreViewState: boolean): Promise { const [part, accessor] = await createPart(restoreViewState); let editor = await accessor.editorService.openEditor(accessor.editorService.createEditorInput({ resource: toResource.call(context, '/path/index.txt'), forceFile: true })); diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts index a869de372..2ce57b49a 100644 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts @@ -94,7 +94,7 @@ suite('Files - TextFileEditorTracker', () => { const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; model.textEditorModel.setValue('Super Good'); - assert.equal(snapshotToString(model.createSnapshot()!), 'Super Good'); + assert.strictEqual(snapshotToString(model.createSnapshot()!), 'Super Good'); await model.save(); @@ -103,7 +103,7 @@ suite('Files - TextFileEditorTracker', () => { await timeout(0); // due to event updating model async - assert.equal(snapshotToString(model.createSnapshot()!), 'Hello Html'); + assert.strictEqual(snapshotToString(model.createSnapshot()!), 'Hello Html'); tracker.dispose(); (accessor.textFileService.files).dispose(); @@ -162,9 +162,7 @@ suite('Files - TextFileEditorTracker', () => { }); function awaitEditorOpening(editorService: IEditorService): Promise { - return new Promise(c => { - Event.once(editorService.onDidActiveEditorChange)(c); - }); + return Event.toPromise(Event.once(editorService.onDidActiveEditorChange)); } test('non-dirty files reload on window focus', async function () { diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index 05c0d93f9..14a4c09a3 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -30,6 +30,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/commonEditorConfig'; import { mergeSort } from 'vs/base/common/arrays'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; type FormattingEditProvider = DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider; @@ -45,6 +46,7 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { @IWorkbenchExtensionEnablementService private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService, @IConfigurationService private readonly _configService: IConfigurationService, @INotificationService private readonly _notificationService: INotificationService, + @IDialogService private readonly _dialogService: IDialogService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @IModeService private readonly _modeService: IModeService, @ILabelService private readonly _labelService: ILabelService, @@ -119,25 +121,32 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { } const langName = this._modeService.getLanguageName(document.getModeId()) || document.getModeId(); - const silent = mode === FormattingMode.Silent; const message = !defaultFormatterId ? nls.localize('config.needed', "There are multiple formatters for '{0}' files. Select a default formatter to continue.", DefaultFormatter._maybeQuotes(langName)) : nls.localize('config.bad', "Extension '{0}' is configured as formatter but not available. Select a different default formatter to continue.", defaultFormatterId); - return new Promise((resolve, reject) => { + if (mode !== FormattingMode.Silent) { + // running from a user action -> show modal dialog so that users configure + // a default formatter + const result = await this._dialogService.confirm({ + message, + primaryButton: nls.localize('do.config', "Configure..."), + secondaryButton: nls.localize('cancel', "Cancel") + }); + if (result.confirmed) { + return this._pickAndPersistDefaultFormatter(formatter, document); + } + + } else { + // no user action -> show a silent notification and proceed this._notificationService.prompt( Severity.Info, message, - [{ label: nls.localize('do.config', "Configure..."), run: () => this._pickAndPersistDefaultFormatter(formatter, document).then(resolve, reject) }], - { silent, onCancel: () => resolve(undefined) } + [{ label: nls.localize('do.config', "Configure..."), run: () => this._pickAndPersistDefaultFormatter(formatter, document) }], + { silent: true } ); - - if (silent) { - // don't wait when formatting happens without interaction - // but pick some formatter... - resolve(undefined); - } - }); + } + return undefined; } private async _pickAndPersistDefaultFormatter(formatter: T[], document: ITextModel): Promise { diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts index f7beaf0f5..94beb1646 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts @@ -6,7 +6,7 @@ import { IssueReporterStyles, IssueReporterData, ProcessExplorerData, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; import { IIssueService } from 'vs/platform/issue/electron-sandbox/issue'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, listHighlightForeground, textLinkActiveForeground, inputValidationErrorBackground, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, textLinkActiveForeground, inputValidationErrorBackground, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -17,6 +17,7 @@ import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; export class WorkbenchIssueService implements IWorkbenchIssueService { declare readonly _serviceBrand: undefined; @@ -28,7 +29,8 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, @IProductService private readonly productService: IProductService, - @ITASExperimentService private readonly experimentService: ITASExperimentService + @ITASExperimentService private readonly experimentService: ITASExperimentService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService ) { } async openReporter(dataOverrides: Partial = {}): Promise { @@ -52,12 +54,15 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { }; }); const experiments = await this.experimentService.getCurrentExperiments(); + const githubSessions = await this.authenticationService.getSessions('github'); + const potentialSessions = githubSessions.filter(session => session.scopes.includes('repo')); const theme = this.themeService.getColorTheme(); const issueReporterData: IssueReporterData = Object.assign({ styles: getIssueReporterStyles(theme), zoomLevel: getZoomLevel(), enabledExtensions: extensionData, - experiments: experiments?.join('\n') + experiments: experiments?.join('\n'), + githubAccessToken: potentialSessions[0]?.accessToken }, dataOverrides); return this.issueService.openReporter(issueReporterData); } @@ -71,8 +76,7 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { backgroundColor: getColor(theme, editorBackground), color: getColor(theme, editorForeground), hoverBackground: getColor(theme, listHoverBackground), - hoverForeground: getColor(theme, listHoverForeground), - highlightForeground: getColor(theme, listHighlightForeground), + hoverForeground: getColor(theme, listHoverForeground) }, platform: process.platform, applicationName: this.productService.applicationName diff --git a/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts b/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts index f8b4c1582..a2dd32715 100644 --- a/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts +++ b/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts @@ -60,7 +60,7 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo updateAndRestart ? localize('updateLocale', "Would you like to change VS Code's UI language to {0} and restart?", e.local.manifest.contributes.localizations[0].languageName || e.local.manifest.contributes.localizations[0].languageId) : localize('activateLanguagePack', "In order to use VS Code in {0}, VS Code needs to restart.", e.local.manifest.contributes.localizations[0].languageName || e.local.manifest.contributes.localizations[0].languageId), [{ - label: updateAndRestart ? localize('yes', "Yes") : localize('restart now', "Restart Now"), + label: updateAndRestart ? localize('changeAndRestart', "Change Language and Restart") : localize('restart', "Restart"), run: () => { const updatePromise = updateAndRestart ? this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['locale'], value: locale }], true) : Promise.resolve(undefined); updatePromise.then(() => this.hostService.restart(), e => this.notificationService.error(e)); diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index 208fea76b..d65e91027 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -4,35 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/workbench/contrib/markers/browser/markersFileDecorations'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { localize } from 'vs/nls'; import { Marker, RelatedInformation } from 'vs/workbench/contrib/markers/browser/markersModel'; import { MarkersView } from 'vs/workbench/contrib/markers/browser/markersView'; -import { MenuId, MenuRegistry, SyncActionDescriptor, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ShowProblemsPanelAction } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import Constants from 'vs/workbench/contrib/markers/browser/constants'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { ActivityUpdater } from 'vs/workbench/contrib/markers/browser/markers'; +import { ActivityUpdater, IMarkersView } from 'vs/workbench/contrib/markers/browser/markers'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { Disposable } from 'vs/base/common/lifecycle'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; import { IMarkerService, MarkerStatistics } from 'vs/platform/markers/common/markers'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry, IViewsService, getVisbileViewContextKey, FocusedViewContext, IViewDescriptorService } from 'vs/workbench/common/views'; +import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry, IViewsService, getVisbileViewContextKey, FocusedViewContext } from 'vs/workbench/common/views'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import type { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ToggleViewAction } from 'vs/workbench/browser/actions/layoutActions'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.MARKER_OPEN_ACTION_ID, @@ -107,24 +105,10 @@ Registry.as(Extensions.Configuration).registerConfigurat } }); -class ToggleMarkersPanelAction extends ToggleViewAction { - - public static readonly ID = 'workbench.actions.view.problems'; - public static readonly LABEL = Messages.MARKERS_PANEL_TOGGLE_LABEL; - - constructor(id: string, label: string, - @IViewsService viewsService: IViewsService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IContextKeyService contextKeyService: IContextKeyService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, Constants.MARKERS_VIEW_ID, viewsService, viewDescriptorService, contextKeyService, layoutService); - } -} - const markersViewIcon = registerIcon('markers-view-icon', Codicon.warning, localize('markersViewIcon', 'View icon of the markers view.')); // markers view container +const TOGGLE_MARKERS_VIEW_ACTION_ID = 'workbench.actions.view.problems'; const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: Constants.MARKERS_CONTAINER_ID, name: Messages.MARKERS_PANEL_TITLE_PROBLEMS, @@ -134,7 +118,8 @@ const VIEW_CONTAINER: ViewContainer = Registry.as(ViewC ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [Constants.MARKERS_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), storageId: Constants.MARKERS_VIEW_STORAGE_ID, focusCommand: { - id: ToggleMarkersPanelAction.ID, keybindings: { + id: TOGGLE_MARKERS_VIEW_ACTION_ID, + keybindings: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M } } @@ -154,12 +139,47 @@ const workbenchRegistry = Registry.as(Workbench workbenchRegistry.registerWorkbenchContribution(ActivityUpdater, LifecyclePhase.Restored); // actions -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleMarkersPanelAction, { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M -}), 'View: Toggle Problems (Errors, Warnings, Infos)', CATEGORIES.View.value); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowProblemsPanelAction), 'View: Focus Problems (Errors, Warnings, Infos)', CATEGORIES.View.value); registerAction2(class extends Action2 { + constructor() { + super({ + id: TOGGLE_MARKERS_VIEW_ACTION_ID, + title: { value: Messages.MARKERS_PANEL_TOGGLE_LABEL, original: 'Toggle Problems (Errors, Warnings, Infos)' }, + category: CATEGORIES.View.value, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M + } + }); + } + async run(accessor: ServicesAccessor): Promise { + return accessor.get(IInstantiationService).createInstance(ToggleViewAction, TOGGLE_MARKERS_VIEW_ACTION_ID, 'Toggle Problems (Errors, Warnings, Infos)', Constants.MARKERS_VIEW_ID).run(); + } +}); +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '4_panels', + command: { + id: TOGGLE_MARKERS_VIEW_ACTION_ID, + title: localize({ key: 'miMarker', comment: ['&& denotes a mnemonic'] }, "&&Problems") + }, + order: 4 +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.problems.focus', + title: { value: Messages.MARKERS_PANEL_SHOW_LABEL, original: 'Focus Problems (Errors, Warnings, Infos)' }, + category: CATEGORIES.View.value, + f1: true, + }); + } + async run(accessor: ServicesAccessor): Promise { + accessor.get(IViewsService).openView(Constants.MARKERS_VIEW_ID, true); + } +}); + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKER_COPY_ACTION_ID, @@ -174,13 +194,19 @@ registerAction2(class extends Action2 { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, when: Constants.MarkerFocusContextKey }, + viewId: Constants.MARKERS_VIEW_ID }); } - async run(accessor: ServicesAccessor) { - await copyMarker(accessor.get(IViewsService), accessor.get(IClipboardService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + const clipboardService = serviceAccessor.get(IClipboardService); + const element = markersView.getFocusElement(); + if (element instanceof Marker) { + await clipboardService.writeText(`${element}`); + } } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKER_COPY_MESSAGE_ACTION_ID, @@ -190,13 +216,19 @@ registerAction2(class extends Action2 { when: Constants.MarkerFocusContextKey, group: 'navigation' }, + viewId: Constants.MARKERS_VIEW_ID }); } - async run(accessor: ServicesAccessor) { - await copyMessage(accessor.get(IViewsService), accessor.get(IClipboardService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + const clipboardService = serviceAccessor.get(IClipboardService); + const element = markersView.getFocusElement(); + if (element instanceof Marker) { + await clipboardService.writeText(element.marker.message); + } } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.RELATED_INFORMATION_COPY_MESSAGE_ACTION_ID, @@ -205,14 +237,20 @@ registerAction2(class extends Action2 { id: MenuId.ProblemsPanelContext, when: Constants.RelatedInformationFocusContextKey, group: 'navigation' - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - async run(accessor: ServicesAccessor) { - await copyRelatedInformationMessage(accessor.get(IViewsService), accessor.get(IClipboardService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + const clipboardService = serviceAccessor.get(IClipboardService); + const element = markersView.getFocusElement(); + if (element instanceof RelatedInformation) { + await clipboardService.writeText(element.raw.message); + } } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.FOCUS_PROBLEMS_FROM_FILTER, @@ -221,14 +259,16 @@ registerAction2(class extends Action2 { when: Constants.MarkerViewFilterFocusContextKey, weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.DownArrow - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - focusProblemsView(accessor.get(IViewsService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.focus(); } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKERS_VIEW_FOCUS_FILTER, @@ -237,14 +277,16 @@ registerAction2(class extends Action2 { when: FocusedViewContext.isEqualTo(Constants.MARKERS_VIEW_ID), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KEY_F - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - focusProblemsFilter(accessor.get(IViewsService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.focusFilter(); } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKERS_VIEW_SHOW_MULTILINE_MESSAGE, @@ -253,17 +295,16 @@ registerAction2(class extends Action2 { menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.has(getVisbileViewContextKey(Constants.MARKERS_VIEW_ID)) - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - const markersView = accessor.get(IViewsService).getActiveViewWithId(Constants.MARKERS_VIEW_ID)!; - if (markersView) { - markersView.markersViewModel.multiline = true; - } + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.setMultiline(true); } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKERS_VIEW_SHOW_SINGLELINE_MESSAGE, @@ -272,17 +313,16 @@ registerAction2(class extends Action2 { menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.has(getVisbileViewContextKey(Constants.MARKERS_VIEW_ID)) - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - const markersView = accessor.get(IViewsService).getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - markersView.markersViewModel.multiline = false; - } + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.setMultiline(false); } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKERS_VIEW_CLEAR_FILTER_TEXT, @@ -291,76 +331,65 @@ registerAction2(class extends Action2 { keybinding: { when: Constants.MarkerViewFilterFocusContextKey, weight: KeybindingWeight.WorkbenchContrib, - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - const markersView = accessor.get(IViewsService).getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - markersView.clearFilterText(); - } + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.clearFilterText(); } }); -async function copyMarker(viewsService: IViewsService, clipboardService: IClipboardService) { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - const element = markersView.getFocusElement(); - if (element instanceof Marker) { - await clipboardService.writeText(`${element}`); - } +registerAction2(class extends ViewAction { + constructor() { + super({ + id: `workbench.actions.treeView.${Constants.MARKERS_VIEW_ID}.collapseAll`, + title: localize('collapseAll', "Collapse All"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', Constants.MARKERS_VIEW_ID), + group: 'navigation', + order: 2, + }, + icon: Codicon.collapseAll, + viewId: Constants.MARKERS_VIEW_ID + }); } -} - -async function copyMessage(viewsService: IViewsService, clipboardService: IClipboardService) { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - const element = markersView.getFocusElement(); - if (element instanceof Marker) { - await clipboardService.writeText(element.marker.message); - } + async runInView(serviceAccessor: ServicesAccessor, view: IMarkersView): Promise { + return view.collapseAll(); } -} - -async function copyRelatedInformationMessage(viewsService: IViewsService, clipboardService: IClipboardService) { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - const element = markersView.getFocusElement(); - if (element instanceof RelatedInformation) { - await clipboardService.writeText(element.raw.message); - } - } -} - -function focusProblemsView(viewsService: IViewsService) { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - markersView.focus(); - } -} - -function focusProblemsFilter(viewsService: IViewsService): void { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - markersView.focusFilter(); - } -} - -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_panels', - command: { - id: ToggleMarkersPanelAction.ID, - title: localize({ key: 'miMarker', comment: ['&& denotes a mnemonic'] }, "&&Problems") - }, - order: 4 }); -CommandsRegistry.registerCommand(Constants.TOGGLE_MARKERS_VIEW_ACTION_ID, async (accessor) => { - const viewsService = accessor.get(IViewsService); - if (viewsService.isViewVisible(Constants.MARKERS_VIEW_ID)) { - viewsService.closeView(Constants.MARKERS_VIEW_ID); - } else { - viewsService.openView(Constants.MARKERS_VIEW_ID, true); +registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.treeView.${Constants.MARKERS_VIEW_ID}.filter`, + title: localize('filter', "Filter"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', Constants.MARKERS_VIEW_ID), Constants.MarkersViewSmallLayoutContextKey.negate()), + group: 'navigation', + order: 1, + }, + }); + } + async run(): Promise { } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: Constants.TOGGLE_MARKERS_VIEW_ACTION_ID, + title: Messages.MARKERS_PANEL_TOGGLE_LABEL, + }); + } + async run(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + if (viewsService.isViewVisible(Constants.MARKERS_VIEW_ID)) { + viewsService.closeView(Constants.MARKERS_VIEW_ID); + } else { + viewsService.openView(Constants.MARKERS_VIEW_ID, true); + } } }); diff --git a/src/vs/workbench/contrib/markers/browser/markers.ts b/src/vs/workbench/contrib/markers/browser/markers.ts index 196235465..10592aba3 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.ts @@ -9,6 +9,26 @@ import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/co import { localize } from 'vs/nls'; import Constants from './constants'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { MarkersFilters } from 'vs/workbench/contrib/markers/browser/markersViewActions'; +import { Event } from 'vs/base/common/event'; +import { IView } from 'vs/workbench/common/views'; +import { MarkerElement } from 'vs/workbench/contrib/markers/browser/markersModel'; + +export interface IMarkersView extends IView { + + readonly onDidFocusFilter: Event; + readonly onDidClearFilterText: Event; + readonly filters: MarkersFilters; + readonly onDidChangeFilterStats: Event<{ total: number, filtered: number }>; + focusFilter(): void; + clearFilterText(): void; + getFilterStats(): { total: number, filtered: number }; + + getFocusElement(): MarkerElement | undefined; + + collapseAll(): void; + setMultiline(multiline: boolean): void; +} export class ActivityUpdater extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/markers/browser/markersModel.ts b/src/vs/workbench/contrib/markers/browser/markersModel.ts index a12e1658e..794791b34 100644 --- a/src/vs/workbench/contrib/markers/browser/markersModel.ts +++ b/src/vs/workbench/contrib/markers/browser/markersModel.ts @@ -14,6 +14,7 @@ import { Hasher } from 'vs/base/common/hash'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { splitLines } from 'vs/base/common/strings'; +export type MarkerElement = ResourceMarkers | Marker | RelatedInformation; export function compareMarkersByUri(a: IMarker, b: IMarker) { return extUri.compare(a.resource, b.resource); diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 6a344c8a0..653b012f4 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -10,7 +10,7 @@ import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { ResourceMarkers, Marker, RelatedInformation } from 'vs/workbench/contrib/markers/browser/markersModel'; +import { ResourceMarkers, Marker, RelatedInformation, MarkerElement } from 'vs/workbench/contrib/markers/browser/markersModel'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; @@ -56,8 +56,6 @@ import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -export type TreeElement = ResourceMarkers | Marker | RelatedInformation; - interface IResourceMarkersTemplateData { resourceLabel: IResourceLabel; count: CountBadge; @@ -74,7 +72,7 @@ interface IRelatedInformationTemplateData { description: HighlightedLabel; } -export class MarkersTreeAccessibilityProvider implements IListAccessibilityProvider { +export class MarkersTreeAccessibilityProvider implements IListAccessibilityProvider { constructor(@ILabelService private readonly labelService: ILabelService) { } @@ -82,7 +80,7 @@ export class MarkersTreeAccessibilityProvider implements IListAccessibilityProvi return localize('problemsView', "Problems View"); } - public getAriaLabel(element: TreeElement): string | null { + public getAriaLabel(element: MarkerElement): string | null { if (element instanceof ResourceMarkers) { const path = this.labelService.getUriLabel(element.resource, { relative: true }) || element.resource.fsPath; return Messages.MARKERS_TREE_ARIA_LABEL_RESOURCE(element.markers.length, element.name, paths.dirname(path)); @@ -103,13 +101,13 @@ const enum TemplateId { RelatedInformation = 'ri' } -export class VirtualDelegate implements IListVirtualDelegate { +export class VirtualDelegate implements IListVirtualDelegate { static LINE_HEIGHT: number = 22; constructor(private readonly markersViewState: MarkersViewModel) { } - getHeight(element: TreeElement): number { + getHeight(element: MarkerElement): number { if (element instanceof Marker) { const viewModel = this.markersViewState.getViewModel(element); const noOfLines = !viewModel || viewModel.multiline ? element.lines.length : 1; @@ -118,7 +116,7 @@ export class VirtualDelegate implements IListVirtualDelegate { return VirtualDelegate.LINE_HEIGHT; } - getTemplateId(element: TreeElement): string { + getTemplateId(element: MarkerElement): string { if (element instanceof ResourceMarkers) { return TemplateId.ResourceMarkers; } else if (element instanceof Marker) { @@ -512,11 +510,11 @@ export class RelatedInformationRenderer implements ITreeRenderer { +export class Filter implements ITreeFilter { constructor(public options: FilterOptions) { } - filter(element: TreeElement, parentVisibility: TreeVisibility): TreeFilterResult { + filter(element: MarkerElement, parentVisibility: TreeVisibility): TreeFilterResult { if (element instanceof ResourceMarkers) { return this.filterResourceMarkers(element); } else if (element instanceof Marker) { @@ -852,23 +850,23 @@ export class MarkersViewModel extends Disposable { } -export class ResourceDragAndDrop implements ITreeDragAndDrop { +export class ResourceDragAndDrop implements ITreeDragAndDrop { constructor( private instantiationService: IInstantiationService ) { } - onDragOver(data: IDragAndDropData, targetElement: TreeElement, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { + onDragOver(data: IDragAndDropData, targetElement: MarkerElement, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { return false; } - getDragURI(element: TreeElement): string | null { + getDragURI(element: MarkerElement): string | null { if (element instanceof ResourceMarkers) { return element.resource.toString(); } return null; } - getDragLabel?(elements: TreeElement[]): string | undefined { + getDragLabel?(elements: MarkerElement[]): string | undefined { if (elements.length > 1) { return String(elements.length); } @@ -877,7 +875,7 @@ export class ResourceDragAndDrop implements ITreeDragAndDrop { } onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { - const elements = (data as ElementsDragAndDropData).elements; + const elements = (data as ElementsDragAndDropData).elements; const resources: URI[] = elements .filter(e => e instanceof ResourceMarkers) .map(resourceMarker => (resourceMarker as ResourceMarkers).resource); @@ -888,7 +886,7 @@ export class ResourceDragAndDrop implements ITreeDragAndDrop { } } - drop(data: IDragAndDropData, targetElement: TreeElement, targetIndex: number, originalEvent: DragEvent): void { + drop(data: IDragAndDropData, targetElement: MarkerElement, targetIndex: number, originalEvent: DragEvent): void { } } diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index 8836c8371..b2a7ce0f6 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -11,17 +11,17 @@ import { IAction, IActionViewItem, Action, Separator } from 'vs/base/common/acti import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import Constants from 'vs/workbench/contrib/markers/browser/constants'; -import { Marker, ResourceMarkers, RelatedInformation, MarkerChangesEvent, MarkersModel, compareMarkersByUri } from 'vs/workbench/contrib/markers/browser/markersModel'; +import { Marker, ResourceMarkers, RelatedInformation, MarkerChangesEvent, MarkersModel, compareMarkersByUri, MarkerElement } from 'vs/workbench/contrib/markers/browser/markersModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MarkersFilterActionViewItem, MarkersFilters, IMarkersFiltersChangeEvent, IMarkerFilterController } from 'vs/workbench/contrib/markers/browser/markersViewActions'; +import { MarkersFilterActionViewItem, MarkersFilters, IMarkersFiltersChangeEvent } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; -import { RangeHighlightDecorations } from 'vs/workbench/browser/parts/editor/rangeDecorations'; +import { RangeHighlightDecorations } from 'vs/workbench/browser/codeeditor'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { localize } from 'vs/nls'; -import { IContextKey, IContextKeyService, ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Iterable } from 'vs/base/common/iterator'; import { ITreeElement, ITreeNode, ITreeContextMenuEvent, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { Relay, Event, Emitter } from 'vs/base/common/event'; @@ -30,10 +30,10 @@ import { FilterOptions } from 'vs/workbench/contrib/markers/browser/markersFilte import { IExpression } from 'vs/base/common/glob'; import { deepClone } from 'vs/base/common/objects'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, TreeElement, MarkersTreeAccessibilityProvider, MarkersViewModel, ResourceDragAndDrop } from 'vs/workbench/contrib/markers/browser/markersTreeViewer'; +import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, MarkersTreeAccessibilityProvider, MarkersViewModel, ResourceDragAndDrop } from 'vs/workbench/contrib/markers/browser/markersTreeViewer'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IMenuService, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { domEvent } from 'vs/base/browser/event'; @@ -45,7 +45,7 @@ import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { KeyCode } from 'vs/base/common/keyCodes'; import { editorLightBulbForeground, editorLightBulbAutoFixForeground } from 'vs/platform/theme/common/colorRegistry'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { Codicon } from 'vs/base/common/codicons'; @@ -55,8 +55,9 @@ import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifec import { groupBy } from 'vs/base/common/arrays'; import { ResourceMap } from 'vs/base/common/map'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IMarkersView } from 'vs/workbench/contrib/markers/browser/markers'; -function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterable> { +function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterable> { return Iterable.map(resourceMarkers.markers, m => { const relatedInformationIt = Iterable.from(m.relatedInformation); const children = Iterable.map(relatedInformationIt, r => ({ element: r })); @@ -65,7 +66,7 @@ function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterab }); } -export class MarkersView extends ViewPane implements IMarkerFilterController { +export class MarkersView extends ViewPane implements IMarkersView { private lastSelectedRelativeTop: number = 0; private currentActiveResource: URI | null = null; @@ -88,7 +89,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private cachedFilterStats: { total: number; filtered: number; } | undefined = undefined; private currentResourceGotAddedToMarkersData: boolean = false; - readonly markersViewModel: MarkersViewModel; + private readonly markersViewModel: MarkersViewModel; private readonly smallLayoutContextKey: IContextKey; private get smallLayout(): boolean { return !!this.smallLayoutContextKey.get(); } private set smallLayout(smallLayout: boolean) { this.smallLayoutContextKey.set(smallLayout); } @@ -132,8 +133,6 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this.filter = new Filter(FilterOptions.EMPTY(uriIdentityService)); this.rangeHighlightDecorations = this._register(this.instantiationService.createInstance(RangeHighlightDecorations)); - // actions - this.regiserActions(); this.filters = this._register(new MarkersFilters({ filterText: this.panelState['filter'] || '', filterHistory: this.panelState['filterHistory'] || [], @@ -208,43 +207,6 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this._onDidClearFilterText.fire(); } - private regiserActions(): void { - const that = this; - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: `workbench.actions.treeView.${that.id}.collapseAll`, - title: localize('collapseAll', "Collapse All"), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyEqualsExpr.create('view', that.id), - group: 'navigation', - order: Number.MAX_SAFE_INTEGER, - }, - icon: Codicon.collapseAll - }); - } - async run(): Promise { - return that.collapseAll(); - } - })); - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: `workbench.actions.treeView.${that.id}.filter`, - title: localize('filter', "Filter"), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), Constants.MarkersViewSmallLayoutContextKey.negate()), - group: 'navigation', - order: 1, - }, - }); - } - async run(): Promise { } - })); - } - public showQuickFixes(marker: Marker): void { const viewModel = this.markersViewModel.getViewModel(marker); if (viewModel) { @@ -423,7 +385,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { const accessibilityProvider = this.instantiationService.createInstance(MarkersTreeAccessibilityProvider); const identityProvider = { - getId(element: TreeElement) { + getId(element: MarkerElement) { return element.id; } }; @@ -438,7 +400,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { accessibilityProvider, identityProvider, dnd: new ResourceDragAndDrop(this.instantiationService), - expandOnlyOnTwistieClick: (e: TreeElement) => e instanceof Marker && e.relatedInformation.length > 0, + expandOnlyOnTwistieClick: (e: MarkerElement) => e instanceof Marker && e.relatedInformation.length > 0, overrideStyles: { listBackground: this.getBackgroundColor() }, @@ -501,7 +463,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this._register(this.tree.onDidChangeSelection(() => this.onSelected())); } - private collapseAll(): void { + collapseAll(): void { if (this.tree) { this.tree.collapseAll(); this.tree.setSelection([]); @@ -511,6 +473,10 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } } + setMultiline(multiline: boolean): void { + this.markersViewModel.multiline = multiline; + } + private onDidChangeMarkersViewVisibility(visible: boolean): void { this.onVisibleDisposables.clear(); if (visible) { @@ -790,7 +756,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this.rangeHighlightDecorations.highlightRange(selection); } - private onContextMenu(e: ITreeContextMenuEvent): void { + private onContextMenu(e: ITreeContextMenuEvent): void { const element = e.element; if (!element) { return; @@ -817,7 +783,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { }); } - private getMenuActions(element: TreeElement): IAction[] { + private getMenuActions(element: MarkerElement): IAction[] { const result: IAction[] = []; if (element instanceof Marker) { @@ -845,8 +811,8 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { return result; } - public getFocusElement() { - return this.tree ? this.tree.getFocus()[0] : undefined; + public getFocusElement(): MarkerElement | undefined { + return this.tree?.getFocus()[0] || undefined; } public getActionViewItem(action: IAction): IActionViewItem | undefined { @@ -924,14 +890,14 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } -class MarkersTree extends WorkbenchObjectTree { +class MarkersTree extends WorkbenchObjectTree { constructor( user: string, readonly container: HTMLElement, - delegate: IListVirtualDelegate, - renderers: ITreeRenderer[], - options: IWorkbenchObjectTreeOptions, + delegate: IListVirtualDelegate, + renderers: ITreeRenderer[], + options: IWorkbenchObjectTreeOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, diff --git a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts index 973e9e697..7dacfc301 100644 --- a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts @@ -24,27 +24,11 @@ import { Marker } from 'vs/workbench/contrib/markers/browser/markersModel'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; -import { IViewsService } from 'vs/workbench/common/views'; import { Codicon } from 'vs/base/common/codicons'; import { BaseActionViewItem, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; - -export class ShowProblemsPanelAction extends Action { - - public static readonly ID = 'workbench.action.problems.focus'; - public static readonly LABEL = Messages.MARKERS_PANEL_SHOW_LABEL; - - constructor(id: string, label: string, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label); - } - - public run(): Promise { - return this.viewsService.openView(Constants.MARKERS_VIEW_ID, true); - } -} +import { IMarkersView } from 'vs/workbench/contrib/markers/browser/markers'; export interface IMarkersFiltersChangeEvent { filterText?: boolean; @@ -164,14 +148,6 @@ export class MarkersFilters extends Disposable { } } -export interface IMarkerFilterController { - readonly onDidFocusFilter: Event; - readonly onDidClearFilterText: Event; - readonly filters: MarkersFilters; - readonly onDidChangeFilterStats: Event<{ total: number, filtered: number }>; - getFilterStats(): { total: number, filtered: number }; -} - class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { constructor( @@ -271,7 +247,7 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { constructor( action: IAction, - private filterController: IMarkerFilterController, + private markersView: IMarkersView, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextViewService private readonly contextViewService: IContextViewService, @IThemeService private readonly themeService: IThemeService, @@ -281,11 +257,11 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { this.focusContextKey = Constants.MarkerViewFilterFocusContextKey.bindTo(contextKeyService); this.delayedFilterUpdate = new Delayer(400); this._register(toDisposable(() => this.delayedFilterUpdate.cancel())); - this._register(filterController.onDidFocusFilter(() => this.focus())); - this._register(filterController.onDidClearFilterText(() => this.clearFilterText())); + this._register(markersView.onDidFocusFilter(() => this.focus())); + this._register(markersView.onDidClearFilterText(() => this.clearFilterText())); this.filtersAction = new Action('markersFiltersAction', Messages.MARKERS_PANEL_ACTION_TOOLTIP_MORE_FILTERS, 'markers-filters ' + ThemeIcon.asClassName(filterIcon)); this.filtersAction.checked = this.hasFiltersChanged(); - this._register(filterController.filters.onDidChange(e => this.onDidFiltersChange(e))); + this._register(markersView.filters.onDidChange(e => this.onDidFiltersChange(e))); } render(container: HTMLElement): void { @@ -321,21 +297,21 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { } private hasFiltersChanged(): boolean { - return !this.filterController.filters.showErrors || !this.filterController.filters.showWarnings || !this.filterController.filters.showInfos || this.filterController.filters.excludedFiles || this.filterController.filters.activeFile; + return !this.markersView.filters.showErrors || !this.markersView.filters.showWarnings || !this.markersView.filters.showInfos || this.markersView.filters.excludedFiles || this.markersView.filters.activeFile; } private createInput(container: HTMLElement): void { this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, { placeholder: Messages.MARKERS_PANEL_FILTER_PLACEHOLDER, ariaLabel: Messages.MARKERS_PANEL_FILTER_ARIA_LABEL, - history: this.filterController.filters.filterHistory + history: this.markersView.filters.filterHistory })); this._register(attachInputBoxStyler(this.filterInputBox, this.themeService)); - this.filterInputBox.value = this.filterController.filters.filterText; + this.filterInputBox.value = this.markersView.filters.filterText; this._register(this.filterInputBox.onDidChange(filter => this.delayedFilterUpdate.trigger(() => this.onDidInputChange(this.filterInputBox!)))); - this._register(this.filterController.filters.onDidChange((event: IMarkersFiltersChangeEvent) => { + this._register(this.markersView.filters.onDidChange((event: IMarkersFiltersChangeEvent) => { if (event.filterText) { - this.filterInputBox!.value = this.filterController.filters.filterText; + this.filterInputBox!.value = this.markersView.filters.filterText; } })); this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.KEY_DOWN, (e: any) => this.onInputKeyDown(e, this.filterInputBox!))); @@ -373,14 +349,14 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { filterBadge.style.color = foreground; })); this.updateBadge(); - this._register(this.filterController.onDidChangeFilterStats(() => this.updateBadge())); + this._register(this.markersView.onDidChangeFilterStats(() => this.updateBadge())); } private createFilters(container: HTMLElement): void { const actionbar = this._register(new ActionBar(container, { actionViewItemProvider: action => { if (action.id === this.filtersAction.id) { - return this.instantiationService.createInstance(FiltersDropdownMenuActionViewItem, action, this.filterController.filters, this.actionRunner); + return this.instantiationService.createInstance(FiltersDropdownMenuActionViewItem, action, this.markersView.filters, this.actionRunner); } return undefined; } @@ -390,13 +366,13 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { private onDidInputChange(inputbox: HistoryInputBox) { inputbox.addToHistory(); - this.filterController.filters.filterText = inputbox.value; - this.filterController.filters.filterHistory = inputbox.getHistory(); + this.markersView.filters.filterText = inputbox.value; + this.markersView.filters.filterHistory = inputbox.getHistory(); } private updateBadge(): void { if (this.filterBadge) { - const { total, filtered } = this.filterController.getFilterStats(); + const { total, filtered } = this.markersView.getFilterStats(); this.filterBadge.classList.toggle('hidden', total === filtered || total === 0); this.filterBadge.textContent = localize('showing filtered problems', "Showing {0} of {1}", filtered, total); this.adjustInputBox(); @@ -441,9 +417,9 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { } protected get class(): string { - if (this.filterController.filters.layout.width > 600) { + if (this.markersView.filters.layout.width > 600) { return 'markers-panel-action-filter grow'; - } else if (this.filterController.filters.layout.width < 400) { + } else if (this.markersView.filters.layout.width < 400) { return 'markers-panel-action-filter small'; } else { return 'markers-panel-action-filter'; diff --git a/src/vs/workbench/contrib/markers/browser/messages.ts b/src/vs/workbench/contrib/markers/browser/messages.ts index 5d7101256..d263f7e10 100644 --- a/src/vs/workbench/contrib/markers/browser/messages.ts +++ b/src/vs/workbench/contrib/markers/browser/messages.ts @@ -19,8 +19,8 @@ export default class Messages { public static MARKERS_PANEL_TITLE_PROBLEMS: string = nls.localize('markers.panel.title.problems', "Problems"); - public static MARKERS_PANEL_NO_PROBLEMS_BUILT: string = nls.localize('markers.panel.no.problems.build', "No problems have been detected in the workspace so far."); - public static MARKERS_PANEL_NO_PROBLEMS_ACTIVE_FILE_BUILT: string = nls.localize('markers.panel.no.problems.activeFile.build', "No problems have been detected in the current file so far."); + public static MARKERS_PANEL_NO_PROBLEMS_BUILT: string = nls.localize('markers.panel.no.problems.build', "No problems have been detected in the workspace."); + public static MARKERS_PANEL_NO_PROBLEMS_ACTIVE_FILE_BUILT: string = nls.localize('markers.panel.no.problems.activeFile.build', "No problems have been detected in the current file."); public static MARKERS_PANEL_NO_PROBLEMS_FILTERS: string = nls.localize('markers.panel.no.problems.filters', "No results found with provided filter criteria."); public static MARKERS_PANEL_ACTION_TOOLTIP_MORE_FILTERS: string = nls.localize('markers.panel.action.moreFilters', "More Filters..."); diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index 3232b0474..ab61c29ec 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -13,8 +13,8 @@ export const CELL_RUN_GUTTER = 28; export const CODE_CELL_LEFT_MARGIN = 32; export const EDITOR_TOOLBAR_HEIGHT = 0; -export const BOTTOM_CELL_TOOLBAR_GAP = 18; -export const BOTTOM_CELL_TOOLBAR_HEIGHT = 50; +export const BOTTOM_CELL_TOOLBAR_GAP = 16; +export const BOTTOM_CELL_TOOLBAR_HEIGHT = 24; export const CELL_STATUSBAR_HEIGHT = 22; // Margin above editor diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index acf32d1a9..9fb714811 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -23,11 +23,17 @@ import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/ import { CATEGORIES } from 'vs/workbench/common/actions'; import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_CONTENT_COMMAND_ID, IActiveNotebookEditor, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellEditType, CellKind, ICellEditOperation, ICellRange, isDocumentExcludePattern, NotebookCellMetadata, NotebookCellRunState, NOTEBOOK_EDITOR_CURSOR_BEGIN_END, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, ICellEditOperation, ICellRange, INotebookDocumentFilter, isDocumentExcludePattern, NotebookCellMetadata, NotebookCellRunState, NOTEBOOK_EDITOR_CURSOR_BEGIN_END, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { EditorsOrder } from 'vs/workbench/common/editor'; +import { INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; // Notebook Commands const EXECUTE_NOTEBOOK_COMMAND_ID = 'notebook.execute'; @@ -137,14 +143,17 @@ abstract class NotebookAction extends Action2 { super(desc); } - async run(accessor: ServicesAccessor, context?: INotebookActionContext): Promise { + async run(accessor: ServicesAccessor, context?: any): Promise { if (!this.isNotebookActionContext(context)) { - context = this.getActiveEditorContext(accessor); + context = this.getEditorContextFromArgsOrActive(accessor, context); if (!context) { return; } } + const telemetryService = accessor.get(ITelemetryService); + telemetryService.publicLog2('workbenchActionExecuted', { id: this.desc.id, from: 'ui' }); + this.runWithContext(accessor, context); } @@ -154,7 +163,7 @@ abstract class NotebookAction extends Action2 { return !!context && !!(context as INotebookActionContext).notebookEditor; } - protected getActiveEditorContext(accessor: ServicesAccessor): INotebookActionContext | undefined { + protected getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: any): INotebookActionContext | undefined { const editorService = accessor.get(IEditorService); const editor = getActiveNotebookEditor(editorService); @@ -179,22 +188,22 @@ abstract class NotebookCellAction extends Notebo return !!context && !!(context as INotebookCellActionContext).notebookEditor && !!(context as INotebookCellActionContext).cell; } - protected getCellContextFromArgs(accessor: ServicesAccessor, context?: T): INotebookCellActionContext | undefined { + protected getCellContextFromArgs(accessor: ServicesAccessor, context?: T, ...additionalArgs: any[]): INotebookCellActionContext | undefined { return undefined; } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext, ...additionalArgs: any[]): Promise { if (this.isCellActionContext(context)) { return this.runWithContext(accessor, context); } - const contextFromArgs = this.getCellContextFromArgs(accessor, context); + const contextFromArgs = this.getCellContextFromArgs(accessor, context, ...additionalArgs); if (contextFromArgs) { return this.runWithContext(accessor, contextFromArgs); } - const activeEditorContext = this.getActiveEditorContext(accessor); + const activeEditorContext = this.getEditorContextFromArgsOrActive(accessor); if (this.isCellActionContext(activeEditorContext)) { return this.runWithContext(accessor, activeEditorContext); } @@ -203,6 +212,22 @@ abstract class NotebookCellAction extends Notebo abstract runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise; } +export function getWidgetFromUri(accessor: ServicesAccessor, uri: URI) { + const editorService = accessor.get(IEditorService); + const notebookWidgetService = accessor.get(INotebookEditorWidgetService); + const editorId = editorService.getEditors(EditorsOrder.SEQUENTIAL).find(editorId => editorId.editor instanceof NotebookEditorInput && editorId.editor.resource?.toString() === uri.toString()); + + if (editorId) { + const widget = notebookWidgetService.widgets.find(widget => widget.textModel?.viewType === (editorId.editor as NotebookEditorInput).viewType && widget.uri?.toString() === editorId.editor.resource!.toString()); + + if (widget && widget.hasModel()) { + return widget; + } + } + + return undefined; +} + registerAction2(class extends NotebookCellAction { constructor() { super({ @@ -234,6 +259,11 @@ registerAction2(class extends NotebookCellAction { } } } + }, + { + name: 'uri', + description: 'The document uri', + constraint: URI } ] }, @@ -241,12 +271,28 @@ registerAction2(class extends NotebookCellAction { }); } - getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange): INotebookCellActionContext | undefined { + getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange, ...additionalArgs: any[]): INotebookCellActionContext | undefined { if (!context || typeof context.start !== 'number' || typeof context.end !== 'number' || context.start >= context.end) { return; } - const activeEditorContext = this.getActiveEditorContext(accessor); + if (additionalArgs.length && additionalArgs[0]) { + const uri = URI.revive(additionalArgs[0]); + + if (uri) { + const widget = getWidgetFromUri(accessor, uri); + if (widget) { + const cells = widget.viewModel.viewCells; + + return { + notebookEditor: widget, + cell: cells[context.start] + }; + } + } + } + + const activeEditorContext = this.getEditorContextFromArgsOrActive(accessor); if (!activeEditorContext || !activeEditorContext.notebookEditor.viewModel || context.start >= activeEditorContext.notebookEditor.viewModel.viewCells.length) { return; @@ -273,7 +319,7 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.cancel', "Stop Cell Execution"), icon: icons.stopIcon, description: { - description: localize('notebookActions.execute', "Execute Cell"), + description: localize('notebookActions.cancel', "Stop Cell Execution"), args: [ { name: 'range', @@ -290,18 +336,39 @@ registerAction2(class extends NotebookCellAction { } } } + }, + { + name: 'uri', + description: 'The document uri', + constraint: URI } ] }, }); } - getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange): INotebookCellActionContext | undefined { + getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange, ...additionalArgs: any[]): INotebookCellActionContext | undefined { if (!context || typeof context.start !== 'number' || typeof context.end !== 'number' || context.start >= context.end) { return; } - const activeEditorContext = this.getActiveEditorContext(accessor); + if (additionalArgs.length && additionalArgs[0]) { + const uri = URI.revive(additionalArgs[0]); + + if (uri) { + const widget = getWidgetFromUri(accessor, uri); + if (widget) { + const cells = widget.viewModel.viewCells; + + return { + notebookEditor: widget, + cell: cells[context.start] + }; + } + } + } + + const activeEditorContext = this.getEditorContextFromArgsOrActive(accessor); if (!activeEditorContext || !activeEditorContext.notebookEditor.viewModel || context.start >= activeEditorContext.notebookEditor.viewModel.viewCells.length) { return; @@ -455,19 +522,62 @@ registerAction2(class extends NotebookAction { super({ id: EXECUTE_NOTEBOOK_COMMAND_ID, title: localize('notebookActions.executeNotebook', "Execute Notebook"), + description: { + description: localize('notebookActions.executeNotebook', "Execute Notebook"), + args: [ + { + name: 'uri', + description: 'The document uri', + constraint: URI + } + ] + }, }); } + getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: UriComponents): INotebookActionContext | undefined { + if (context) { + const uri = URI.revive(context); + + if (uri) { + const widget = getWidgetFromUri(accessor, uri); + + if (widget) { + return { + notebookEditor: widget, + }; + } + } + } + + const editorService = accessor.get(IEditorService); + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + if (!editor.hasModel()) { + return; + } + + const activeCell = editor.getActiveCell(); + return { + cell: activeCell, + notebookEditor: editor + }; + } + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { renderAllMarkdownCells(context); + const editorService = accessor.get(IEditorService); + const editor = editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).find( + editor => editor.editor instanceof NotebookEditorInput && editor.editor.viewType === context.notebookEditor.viewModel.viewType && editor.editor.resource.toString() === context.notebookEditor.viewModel.uri.toString()); const editorGroupService = accessor.get(IEditorGroupsService); - const group = editorGroupService.activeGroup; - if (group) { - if (group.activeEditor) { - group.pinEditor(group.activeEditor); - } + if (editor) { + const group = editorGroupService.getGroup(editor.groupId); + group?.pinEditor(editor.editor); } return context.notebookEditor.executeNotebook(); @@ -487,9 +597,51 @@ registerAction2(class extends NotebookAction { super({ id: CANCEL_NOTEBOOK_COMMAND_ID, title: localize('notebookActions.cancelNotebook', "Cancel Notebook Execution"), + description: { + description: localize('notebookActions.cancelNotebook', "Cancel Notebook Execution"), + args: [ + { + name: 'uri', + description: 'The document uri', + constraint: URI + } + ] + }, }); } + getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: UriComponents): INotebookActionContext | undefined { + if (context) { + const uri = URI.revive(context); + + if (uri) { + const widget = getWidgetFromUri(accessor, uri); + + if (widget) { + return { + notebookEditor: widget, + }; + } + } + } + + const editorService = accessor.get(IEditorService); + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + if (!editor.hasModel()) { + return; + } + + const activeCell = editor.getActiveCell(); + return { + cell: activeCell, + notebookEditor: editor + }; + } + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { return context.notebookEditor.cancelNotebookExecution(); } @@ -580,7 +732,7 @@ registerAction2(class extends NotebookCellAction { export function getActiveNotebookEditor(editorService: IEditorService): INotebookEditor | undefined { // TODO@roblourens can `isNotebookEditor` be on INotebookEditor to avoid a circular dependency? - const activeEditorPane = editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean } | undefined; + const activeEditorPane = editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean; } | undefined; return activeEditorPane?.isNotebookEditor ? (editorService.activeEditorPane?.getControl() as INotebookEditor) : undefined; } @@ -709,7 +861,7 @@ registerAction2(class extends NotebookAction { } async run(accessor: ServicesAccessor): Promise { - const context = this.getActiveEditorContext(accessor); + const context = this.getEditorContextFromArgsOrActive(accessor); if (context) { this.runWithContext(accessor, context); } @@ -734,7 +886,7 @@ registerAction2(class extends NotebookAction { } async run(accessor: ServicesAccessor): Promise { - const context = this.getActiveEditorContext(accessor); + const context = this.getEditorContextFromArgsOrActive(accessor); if (context) { this.runWithContext(accessor, context); } @@ -1891,8 +2043,8 @@ CommandsRegistry.registerCommand('notebook.trust', (accessor, args) => { CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, args): { viewType: string; displayName: string; - options: { transientOutputs: boolean; transientMetadata: TransientMetadata }; - filenamePattern: (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern })[] + options: { transientOutputs: boolean; transientMetadata: TransientMetadata; }; + filenamePattern: (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern; })[]; }[] => { const notebookService = accessor.get(INotebookService); const contentProviders = notebookService.getContributedNotebookProviders(); @@ -1914,7 +2066,7 @@ CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, a } return null; - }).filter(pattern => pattern !== null) as (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern })[]; + }).filter(pattern => pattern !== null) as (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern; })[]; return { viewType: provider.id, @@ -1924,3 +2076,44 @@ CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, a }; }); }); + +CommandsRegistry.registerCommand('_resolveNotebookKernelProviders', async (accessor, args): Promise<{ + extensionId: string; + description?: string; + selector: INotebookDocumentFilter; +}[]> => { + const notebookService = accessor.get(INotebookService); + const providers = await notebookService.getContributedNotebookKernelProviders(); + return providers.map(provider => ({ + extensionId: provider.providerExtensionId, + description: provider.providerDescription, + selector: provider.selector + })); +}); + +CommandsRegistry.registerCommand('_resolveNotebookKernels', async (accessor, args: { + viewType: string; + uri: UriComponents; +}): Promise<{ + id?: string; + label: string; + description?: string; + detail?: string; + isPreferred?: boolean; + preloads?: URI[]; +}[]> => { + const notebookService = accessor.get(INotebookService); + const uri = URI.revive(args.uri as UriComponents); + const source = new CancellationTokenSource(); + const kernels = await notebookService.getContributedNotebookKernels(args.viewType, uri, source.token); + source.dispose(); + + return kernels.map(provider => ({ + id: provider.friendlyId, + label: provider.label, + description: provider.description, + detail: provider.detail, + isPreferred: provider.isPreferred, + preloads: provider.preloads?.map(preload => URI.revive(preload)) || [] + })); +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts index 2c2c46b89..c7f0f53cd 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/notebookFind'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, INotebookEditor, CellFindMatch, CellEditState, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, INotebookEditor, CellFindMatch, CellEditState, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_OPEN } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { FindDecorations } from 'vs/editor/contrib/find/findDecorations'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; @@ -385,7 +385,7 @@ registerAction2(class extends Action2 { id: 'notebook.find', title: { value: localize('notebookActions.findInNotebook', "Find in Notebook"), original: 'Find in Notebook' }, keybinding: { - when: NOTEBOOK_EDITOR_FOCUSED, + when: ContextKeyExpr.or(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_OPEN), primary: KeyCode.KEY_F | KeyMod.CtrlCmd, weight: KeybindingWeight.WorkbenchContrib } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.css b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.css new file mode 100644 index 000000000..6e71e660f --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.css @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-list .notebook-outline-element { + display: flex; + flex: 1; + flex-flow: row nowrap; + align-items: center; +} + +.monaco-list .notebook-outline-element > .element-icon.file-icon { + height: 100%; +} + +.monaco-breadcrumbs > .notebook-outline-element > .element-icon.file-icon { + height: 18px; +} +.monaco-list .notebook-outline-element .monaco-highlighted-label { + color: var(--outline-element-color); +} + +.monaco-breadcrumbs .notebook-outline-element .element-decoration, +.monaco-list .notebook-outline-element > .element-decoration { + opacity: 0.75; + font-size: 90%; + font-weight: 600; + padding: 0 12px 0 5px; + margin-left: auto; + text-align: center; + color: var(--outline-element-color); +} + +.monaco-list .notebook-outline-element > .element-decoration.bubble { + font-family: codicon; + font-size: 14px; + opacity: 0.4; + padding-right: 8px; +} + +.monaco-breadcrumbs .notebook-outline-element .element-decoration { + /* Don't show markers inline with breadcrumbs */ + display: none; +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts new file mode 100644 index 000000000..f6c2dd201 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -0,0 +1,607 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./notebookOutline'; +import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; +import { combinedDisposable, IDisposable, Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IOutline, IOutlineComparator, IOutlineCreator, IOutlineListConfig, IOutlineService, IQuickPickDataSource, IQuickPickOutlineElement, OutlineChangeEvent, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IEditorPane } from 'vs/workbench/common/editor'; +import { IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { createMatches, FuzzyScore } from 'vs/base/common/filters'; +import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { getIconClassesForModeId } from 'vs/editor/common/services/getIconClasses'; +import { IWorkbenchDataTreeOptions } from 'vs/platform/list/browser/listService'; +import { localize } from 'vs/nls'; +import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/common/colorRegistry'; +import { isEqual } from 'vs/base/common/resources'; +import { IdleValue } from 'vs/base/common/async'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import * as marked from 'vs/base/common/marked/marked'; +import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; + +export interface IOutlineMarkerInfo { + readonly count: number; + readonly topSev: MarkerSeverity; +} + +export class OutlineEntry { + + private _children: OutlineEntry[] = []; + private _parent: OutlineEntry | undefined; + private _markerInfo: IOutlineMarkerInfo | undefined; + + constructor( + readonly index: number, + readonly level: number, + readonly cell: ICellViewModel, + readonly label: string, + readonly icon: ThemeIcon + ) { } + + addChild(entry: OutlineEntry) { + this._children.push(entry); + entry._parent = this; + } + + get parent(): OutlineEntry | undefined { + return this._parent; + } + + get children(): Iterable { + return this._children; + } + + get markerInfo(): IOutlineMarkerInfo | undefined { + return this._markerInfo; + } + + updateMarkers(markerService: IMarkerService): void { + if (this.cell.cellKind === CellKind.Code) { + // a code cell can have marker + const marker = markerService.read({ resource: this.cell.uri, severities: MarkerSeverity.Error | MarkerSeverity.Warning }); + if (marker.length === 0) { + this._markerInfo = undefined; + } else { + const topSev = marker.find(a => a.severity === MarkerSeverity.Error)?.severity ?? MarkerSeverity.Warning; + this._markerInfo = { topSev, count: marker.length }; + } + } else { + // a markdown cell can inherit markers from its children + let topChild: MarkerSeverity | undefined; + for (let child of this.children) { + child.updateMarkers(markerService); + if (child.markerInfo) { + topChild = !topChild ? child.markerInfo.topSev : Math.max(child.markerInfo.topSev, topChild); + } + } + this._markerInfo = topChild && { topSev: topChild, count: 0 }; + } + } + + clearMarkers(): void { + this._markerInfo = undefined; + for (let child of this.children) { + child.clearMarkers(); + } + } + + find(cell: ICellViewModel, parents: OutlineEntry[]): OutlineEntry | undefined { + if (cell.id === this.cell.id) { + return this; + } + parents.push(this); + for (let child of this.children) { + const result = child.find(cell, parents); + if (result) { + return result; + } + } + parents.pop(); + return undefined; + } + + asFlatList(bucket: OutlineEntry[]): void { + bucket.push(this); + for (let child of this.children) { + child.asFlatList(bucket); + } + } +} + +class NotebookOutlineTemplate { + + static readonly templateId = 'NotebookOutlineRenderer'; + + constructor( + readonly container: HTMLElement, + readonly iconClass: HTMLElement, + readonly iconLabel: IconLabel, + readonly decoration: HTMLElement + ) { } +} + +class NotebookOutlineRenderer implements ITreeRenderer { + + templateId: string = NotebookOutlineTemplate.templateId; + + constructor( + @IThemeService private readonly _themeService: IThemeService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { } + + renderTemplate(container: HTMLElement): NotebookOutlineTemplate { + container.classList.add('notebook-outline-element', 'show-file-icons'); + const iconClass = document.createElement('div'); + container.append(iconClass); + const iconLabel = new IconLabel(container, { supportHighlights: true }); + const decoration = document.createElement('div'); + decoration.className = 'element-decoration'; + container.append(decoration); + return new NotebookOutlineTemplate(container, iconClass, iconLabel, decoration); + } + + renderElement(node: ITreeNode, _index: number, template: NotebookOutlineTemplate, _height: number | undefined): void { + const options: IIconLabelValueOptions = { + matches: createMatches(node.filterData), + extraClasses: [] + }; + + if (node.element.cell.cellKind === CellKind.Code && this._themeService.getFileIconTheme().hasFileIcons) { + template.iconClass.className = ''; + options.extraClasses?.push(...getIconClassesForModeId(node.element.cell.language ?? '')); + } else { + template.iconClass.className = 'element-icon ' + ThemeIcon.asClassNameArray(node.element.icon).join(' '); + } + + template.iconLabel.setLabel(node.element.label, undefined, options); + + const { markerInfo } = node.element; + + template.container.style.removeProperty('--outline-element-color'); + template.decoration.innerText = ''; + if (markerInfo) { + const useBadges = this._configurationService.getValue(OutlineConfigKeys.problemsBadges); + if (!useBadges) { + template.decoration.classList.remove('bubble'); + template.decoration.innerText = ''; + } else if (markerInfo.count === 0) { + template.decoration.classList.add('bubble'); + template.decoration.innerText = '\uea71'; + } else { + template.decoration.classList.remove('bubble'); + template.decoration.innerText = markerInfo.count > 9 ? '9+' : String(markerInfo.count); + } + const color = this._themeService.getColorTheme().getColor(markerInfo.topSev === MarkerSeverity.Error ? listErrorForeground : listWarningForeground); + const useColors = this._configurationService.getValue(OutlineConfigKeys.problemsColors); + if (!useColors) { + template.container.style.removeProperty('--outline-element-color'); + template.decoration.style.setProperty('--outline-element-color', color?.toString() ?? 'inherit'); + } else { + template.container.style.setProperty('--outline-element-color', color?.toString() ?? 'inherit'); + } + } + } + + disposeTemplate(templateData: NotebookOutlineTemplate): void { + templateData.iconLabel.dispose(); + } +} + +class NotebookOutlineAccessibility implements IListAccessibilityProvider { + getAriaLabel(element: OutlineEntry): string | null { + return element.label; + } + getWidgetAriaLabel(): string { + return ''; + } +} + +class NotebookNavigationLabelProvider implements IKeyboardNavigationLabelProvider { + getKeyboardNavigationLabel(element: OutlineEntry): { toString(): string | undefined; } | { toString(): string | undefined; }[] | undefined { + return element.label; + } +} + +class NotebookOutlineVirtualDelegate implements IListVirtualDelegate { + + getHeight(_element: OutlineEntry): number { + return 22; + } + + getTemplateId(_element: OutlineEntry): string { + return NotebookOutlineTemplate.templateId; + } +} + +class NotebookQuickPickProvider implements IQuickPickDataSource { + + constructor( + private _getEntries: () => OutlineEntry[], + @IThemeService private readonly _themeService: IThemeService + ) { } + + getQuickPickElements(): Iterable> { + const bucket: OutlineEntry[] = []; + for (let entry of this._getEntries()) { + entry.asFlatList(bucket); + } + const result: IQuickPickOutlineElement[] = []; + const { hasFileIcons } = this._themeService.getFileIconTheme(); + for (let element of bucket) { + // todo@jrieken it is fishy that codicons cannot be used with iconClasses + // but file icons can... + result.push({ + element, + label: hasFileIcons ? element.label : `$(${element.icon.id}) ${element.label}`, + ariaLabel: element.label, + iconClasses: hasFileIcons ? getIconClassesForModeId(element.cell.language ?? '') : undefined, + }); + } + return result; + } +} + +class NotebookComparator implements IOutlineComparator { + + private readonly _collator = new IdleValue(() => new Intl.Collator(undefined, { numeric: true })); + + compareByPosition(a: OutlineEntry, b: OutlineEntry): number { + return a.index - b.index; + } + compareByType(a: OutlineEntry, b: OutlineEntry): number { + return a.cell.cellKind - b.cell.cellKind || this._collator.value.compare(a.label, b.label); + } + compareByName(a: OutlineEntry, b: OutlineEntry): number { + return this._collator.value.compare(a.label, b.label); + } +} + +class NotebookCellOutline implements IOutline { + + private readonly _dispoables = new DisposableStore(); + + private readonly _onDidChange = new Emitter(); + + readonly onDidChange: Event = this._onDidChange.event; + + private _entries: OutlineEntry[] = []; + private _activeEntry?: OutlineEntry; + private readonly _entriesDisposables = new DisposableStore(); + + readonly config: IOutlineListConfig; + readonly outlineKind = 'notebookCells'; + + get activeElement(): OutlineEntry | undefined { + return this._activeEntry; + } + + constructor( + private readonly _editor: NotebookEditor, + private readonly _target: OutlineTarget, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IEditorService private readonly _editorService: IEditorService, + @IMarkerService private readonly _markerService: IMarkerService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + const selectionListener = new MutableDisposable(); + this._dispoables.add(selectionListener); + const installSelectionListener = () => { + if (!_editor.viewModel) { + selectionListener.clear(); + } else { + selectionListener.value = combinedDisposable( + _editor.viewModel.onDidChangeSelection(() => this._recomputeActive()), + _editor.viewModel.onDidChangeViewCells(() => this._recomputeState()) + ); + } + }; + + this._dispoables.add(_editor.onDidChangeModel(() => { + this._recomputeState(); + installSelectionListener(); + })); + + this._dispoables.add(_configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('notebook.outline.showCodeCells')) { + this._recomputeState(); + } + })); + + this._dispoables.add(themeService.onDidFileIconThemeChange(() => { + this._onDidChange.fire({}); + })); + + this._recomputeState(); + installSelectionListener(); + + const options: IWorkbenchDataTreeOptions = { + collapseByDefault: _target === OutlineTarget.Breadcrumbs, + expandOnlyOnTwistieClick: true, + multipleSelectionSupport: false, + accessibilityProvider: new NotebookOutlineAccessibility(), + identityProvider: { getId: element => element.cell.id }, + keyboardNavigationLabelProvider: new NotebookNavigationLabelProvider() + }; + + const treeDataSource: IDataSource = { getChildren: parent => parent instanceof NotebookCellOutline ? this._entries : parent.children }; + const delegate = new NotebookOutlineVirtualDelegate(); + const renderers = [instantiationService.createInstance(NotebookOutlineRenderer)]; + const comparator = new NotebookComparator(); + + this.config = { + breadcrumbsDataSource: { + getBreadcrumbElements: () => { + let result: OutlineEntry[] = []; + let candidate = this._activeEntry; + while (candidate) { + result.unshift(candidate); + candidate = candidate.parent; + } + return result; + } + }, + quickPickDataSource: instantiationService.createInstance(NotebookQuickPickProvider, () => this._entries), + treeDataSource, + delegate, + renderers, + comparator, + options + }; + } + + dispose(): void { + this._dispoables.dispose(); + this._entriesDisposables.dispose(); + } + + private _recomputeState(): void { + this._entriesDisposables.clear(); + this._activeEntry = undefined; + this._entries.length = 0; + + const { viewModel } = this._editor; + if (!viewModel) { + return; + } + + let includeCodeCells = true; + if (this._target === OutlineTarget.OutlinePane) { + includeCodeCells = this._configurationService.getValue('notebook.outline.showCodeCells'); + } else if (this._target === OutlineTarget.Breadcrumbs) { + includeCodeCells = this._configurationService.getValue('notebook.breadcrumbs.showCodeCells'); + } + + const [selected] = viewModel.selectionHandles; + const entries: OutlineEntry[] = []; + + for (let i = 0; i < viewModel.viewCells.length; i++) { + const cell = viewModel.viewCells[i]; + const isMarkdown = cell.cellKind === CellKind.Markdown; + if (!isMarkdown && !includeCodeCells) { + continue; + } + + // anaslse cell text but cap it 10000 characters + let content = cell.getText().substr(0, 10_000); + let level = 7; + + if (isMarkdown) { + // MD cell: "render" as plain text, find highest header + for (const token of marked.lexer(content, { gfm: true })) { + if (token.type === 'heading') { + level = Math.min(level, token.depth); + } + } + content = renderMarkdownAsPlaintext({ value: content }); + } + + // find first none empty line or use default text + const lineMatch = content.match(/^.*\w+.*\w*$/m); + let preview: string; + if (!lineMatch) { + preview = localize('empty', "empty cell"); + } else { + preview = lineMatch[0].trim(); + if (preview.length >= 64) { + preview = preview.slice(0, 64) + '…'; + } + } + + const entry = new OutlineEntry(i, level, cell, preview, isMarkdown ? Codicon.markdown : Codicon.code); + entries.push(entry); + if (cell.handle === selected) { + this._activeEntry = entry; + } + + // send an event whenever any of the cells change + this._entriesDisposables.add(cell.model.onDidChangeContent(() => { + this._recomputeState(); + this._onDidChange.fire({}); + })); + } + + // build a tree from the list of entries + if (entries.length > 0) { + let result: OutlineEntry[] = [entries[0]]; + let parentStack: OutlineEntry[] = [entries[0]]; + + for (let i = 1; i < entries.length; i++) { + let entry = entries[i]; + + while (true) { + const len = parentStack.length; + if (len === 0) { + // root node + result.push(entry); + parentStack.push(entry); + break; + + } else { + let parentCandidate = parentStack[len - 1]; + if (parentCandidate.level < entry.level) { + parentCandidate.addChild(entry); + parentStack.push(entry); + break; + } else { + parentStack.pop(); + } + } + } + } + this._entries = result; + } + + // feature: show markers with each cell + const markerServiceListener = new MutableDisposable(); + this._entriesDisposables.add(markerServiceListener); + const updateMarkerUpdater = () => { + const doUpdateMarker = (clear: boolean) => { + for (let entry of this._entries) { + if (clear) { + entry.clearMarkers(); + } else { + entry.updateMarkers(this._markerService); + } + } + }; + if (this._configurationService.getValue(OutlineConfigKeys.problemsEnabled)) { + markerServiceListener.value = this._markerService.onMarkerChanged(e => { + if (e.some(uri => viewModel.viewCells.some(cell => isEqual(cell.uri, uri)))) { + doUpdateMarker(false); + this._onDidChange.fire({}); + } + }); + doUpdateMarker(false); + } else { + markerServiceListener.clear(); + doUpdateMarker(true); + } + }; + updateMarkerUpdater(); + this._entriesDisposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) { + updateMarkerUpdater(); + this._onDidChange.fire({}); + } + })); + + this._onDidChange.fire({}); + } + + private _recomputeActive(): void { + let newActive: OutlineEntry | undefined; + const { viewModel } = this._editor; + + if (viewModel) { + const [selected] = viewModel.selectionHandles; + const cell = viewModel.getCellByHandle(selected); + if (cell) { + for (let entry of this._entries) { + newActive = entry.find(cell, []); + if (newActive) { + break; + } + } + } + } + if (newActive !== this._activeEntry) { + this._activeEntry = newActive; + this._onDidChange.fire({ affectOnlyActiveElement: true }); + } + } + + get isEmpty(): boolean { + return this._entries.length === 0; + } + + async reveal(entry: OutlineEntry, options: IEditorOptions, sideBySide: boolean): Promise { + await this._editorService.openEditor({ + resource: entry.cell.uri, + options, + }, sideBySide ? SIDE_GROUP : undefined); + } + + preview(entry: OutlineEntry): IDisposable { + const widget = this._editor.getControl(); + if (!widget) { + return Disposable.None; + } + widget.revealInCenterIfOutsideViewport(entry.cell); + const ids = widget.deltaCellDecorations([], [{ + handle: entry.cell.handle, + options: { className: 'nb-symbolHighlight', outputClassName: 'nb-symbolHighlight' } + }]); + return toDisposable(() => { widget.deltaCellDecorations(ids, []); }); + + } + + captureViewState(): IDisposable { + const widget = this._editor.getControl(); + let viewState = widget?.getEditorViewState(); + return toDisposable(() => { + if (viewState) { + widget?.restoreListViewState(viewState); + } + }); + } +} + +class NotebookOutlineCreator implements IOutlineCreator { + + readonly dispose: () => void; + + constructor( + @IOutlineService outlineService: IOutlineService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + const reg = outlineService.registerOutlineCreator(this); + this.dispose = () => reg.dispose(); + } + + matches(candidate: IEditorPane): candidate is NotebookEditor { + return candidate.getId() === NotebookEditor.ID; + } + + async createOutline(editor: NotebookEditor, target: OutlineTarget): Promise | undefined> { + return this._instantiationService.createInstance(NotebookCellOutline, editor, target); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NotebookOutlineCreator, LifecyclePhase.Eventually); + + +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + id: 'notebook', + order: 100, + type: 'object', + 'properties': { + 'notebook.outline.showCodeCells': { + type: 'boolean', + default: false, + markdownDescription: localize('outline.showCodeCells', "When enabled notebook outline shows code cells.") + }, + 'notebook.breadcrumbs.showCodeCells': { + type: 'boolean', + default: true, + markdownDescription: localize('breadcrumbs.showCodeCells', "When enabled notebook breadcrumbs contain code cells.") + }, + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts index 2f32225cb..d3bc0b2ab 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -8,7 +8,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { INotebookActionContext, NOTEBOOK_ACTIONS_CATEGORY, getActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { NOTEBOOK_ACTIONS_CATEGORY, getActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; import { INotebookEditor, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -33,11 +33,33 @@ registerAction2(class extends Action2 { title: { value: nls.localize('notebookActions.selectKernel', "Select Notebook Kernel"), original: 'Select Notebook Kernel' }, precondition: NOTEBOOK_IS_ACTIVE_EDITOR, icon: selectKernelIcon, - f1: true + f1: true, + description: { + description: nls.localize('notebookActions.selectKernel.args', "Notebook Kernel Args"), + args: [ + { + name: 'kernelInfo', + description: 'The kernel info', + schema: { + 'type': 'object', + 'required': ['id', 'extension'], + 'properties': { + 'id': { + 'type': 'string' + }, + 'extension': { + 'type': 'string' + } + } + } + } + ] + }, + }); } - async run(accessor: ServicesAccessor, context?: INotebookActionContext): Promise { + async run(accessor: ServicesAccessor, context?: { id: string, extension: string }): Promise { const editorService = accessor.get(IEditorService); const quickInputService = accessor.get(IQuickInputService); const configurationService = accessor.get(IConfigurationService); @@ -52,20 +74,38 @@ registerAction2(class extends Action2 { const picker = quickInputService.createQuickPick<(IQuickPickItem & { run(): void; kernelProviderId?: string })>(); picker.placeholder = nls.localize('notebook.runCell.selectKernel', "Select a notebook kernel to run this notebook"); picker.matchOnDetail = true; - picker.show(); + + + if (context && context.id) { + } else { + picker.show(); + } + picker.busy = true; const tokenSource = new CancellationTokenSource(); - const availableKernels2 = await editor.beginComputeContributedKernels(); - const picks: QuickPickInput[] = [...availableKernels2].map((a) => { + const availableKernels = await editor.beginComputeContributedKernels(); + + const selectedKernel = availableKernels.length ? availableKernels.find( + kernel => kernel.id && context?.id && kernel.id === context?.id && kernel.extension.value === context?.extension + ) : undefined; + + if (selectedKernel) { + editor.activeKernel = selectedKernel!; + return selectedKernel!.resolve(editor.uri!, editor.getId(), tokenSource.token); + } else { + picker.show(); + } + + const picks: QuickPickInput[] = [...availableKernels].map((a) => { return { - id: a.id, + id: a.friendlyId, label: a.label, - picked: a.id === activeKernel?.id, + picked: a.friendlyId === activeKernel?.friendlyId, description: a.description ? a.description - : a.extension.value + (a.id === activeKernel?.id + : a.extension.value + (a.friendlyId === activeKernel?.friendlyId ? nls.localize('currentActiveKernel', " (Currently Active)") : ''), detail: a.detail, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts b/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts deleted file mode 100644 index 48599557f..000000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { TableOfContentsProviderRegistry, ITableOfContentsProvider, ITableOfContentsEntry } from 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess'; -import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { Codicon } from 'vs/base/common/codicons'; -import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; - -TableOfContentsProviderRegistry.register(NotebookEditor.ID, new class implements ITableOfContentsProvider { - async provideTableOfContents(editor: NotebookEditor, context: { disposables: DisposableStore }) { - if (!editor.viewModel) { - return undefined; - } - // return an entry per markdown header - const notebookWidget = editor.getControl(); - if (!notebookWidget) { - return undefined; - } - - // restore initial view state when no item was picked - let didPickOne = false; - const viewState = notebookWidget.getEditorViewState(); - context.disposables.add(toDisposable(() => { - if (!didPickOne) { - notebookWidget.restoreListViewState(viewState); - } - })); - - let lastDecorationId: string[] = []; - const result: ITableOfContentsEntry[] = []; - for (const cell of editor.viewModel.viewCells) { - const content = cell.getText(); - const regexp = cell.cellKind === CellKind.Markdown - ? /^[ \t]*(\#+)(.+)$/gm // md: header - : /^.*\w+.*\w*$/m; // code: none empty line - - const matches = content.match(regexp); - if (matches && matches.length) { - for (let j = 0; j < matches.length; j++) { - result.push({ - icon: cell.cellKind === CellKind.Markdown ? Codicon.markdown : Codicon.code, - label: matches[j].replace(/^[ \t]*(\#+)/, ''), - pick() { - didPickOne = true; - notebookWidget.revealInCenterIfOutsideViewport(cell); - notebookWidget.selectElement(cell); - notebookWidget.focusNotebookCell(cell, cell.cellKind === CellKind.Markdown ? 'container' : 'editor'); - lastDecorationId = notebookWidget.deltaCellDecorations(lastDecorationId, []); - }, - preview() { - notebookWidget.revealInCenterIfOutsideViewport(cell); - notebookWidget.selectElement(cell); - lastDecorationId = notebookWidget.deltaCellDecorations(lastDecorationId, [{ - handle: cell.handle, - options: { className: 'nb-symbolHighlight', outputClassName: 'nb-symbolHighlight' } - }]); - } - }); - } - } - } - - context.disposables.add(toDisposable(() => { - notebookWidget.deltaCellDecorations(lastDecorationId, []); - })); - - return result; - } -}); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts deleted file mode 100644 index f526080b2..000000000 --- a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts +++ /dev/null @@ -1,1091 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as DOM from 'vs/base/browser/dom'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CellDiffViewModel, PropertyFoldingState } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; -import { CellDiffRenderTemplate, CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; -import { EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { format } from 'vs/base/common/jsonFormatter'; -import { applyEdits } from 'vs/base/common/jsonEdit'; -import { CellEditType, CellUri, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { hash } from 'vs/base/common/hash'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IAction } from 'vs/base/common/actions'; -import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; -import { getEditorTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { collapsedIcon, expandedIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; -import { renderCodicons } from 'vs/base/browser/codicons'; - -const fixedEditorOptions: IEditorOptions = { - padding: { - top: 12, - bottom: 12 - }, - scrollBeyondLastLine: false, - scrollbar: { - verticalScrollbarSize: 14, - horizontal: 'auto', - useShadows: true, - verticalHasArrows: false, - horizontalHasArrows: false, - alwaysConsumeMouseWheel: false - }, - renderLineHighlightOnlyWhenFocus: true, - overviewRulerLanes: 0, - selectOnLineNumbers: false, - wordWrap: 'off', - lineNumbers: 'off', - lineDecorationsWidth: 0, - glyphMargin: false, - fixedOverflowWidgets: true, - minimap: { enabled: false }, - renderValidationDecorations: 'on', - renderLineHighlight: 'none', - readOnly: true -}; - -const fixedDiffEditorOptions: IDiffEditorOptions = { - ...fixedEditorOptions, - glyphMargin: true, - enableSplitViewResizing: false, - renderIndicators: false, - readOnly: false, - isInEmbeddedEditor: true -}; - - - -class PropertyHeader extends Disposable { - protected _foldingIndicator!: HTMLElement; - protected _statusSpan!: HTMLElement; - protected _toolbar!: ToolBar; - protected _menu!: IMenu; - - constructor( - readonly cell: CellDiffViewModel, - readonly metadataHeaderContainer: HTMLElement, - readonly notebookEditor: INotebookTextDiffEditor, - readonly accessor: { - updateInfoRendering: () => void; - checkIfModified: (cell: CellDiffViewModel) => boolean; - getFoldingState: (cell: CellDiffViewModel) => PropertyFoldingState; - updateFoldingState: (cell: CellDiffViewModel, newState: PropertyFoldingState) => void; - unChangedLabel: string; - changedLabel: string; - prefix: string; - menuId: MenuId; - }, - @IContextMenuService readonly contextMenuService: IContextMenuService, - @IKeybindingService readonly keybindingService: IKeybindingService, - @INotificationService readonly notificationService: INotificationService, - @IMenuService readonly menuService: IMenuService, - @IContextKeyService readonly contextKeyService: IContextKeyService - ) { - super(); - } - - buildHeader(): void { - let metadataChanged = this.accessor.checkIfModified(this.cell); - this._foldingIndicator = DOM.append(this.metadataHeaderContainer, DOM.$('.property-folding-indicator')); - this._foldingIndicator.classList.add(this.accessor.prefix); - this._updateFoldingIcon(); - const metadataStatus = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-status')); - this._statusSpan = DOM.append(metadataStatus, DOM.$('span')); - - if (metadataChanged) { - this._statusSpan.textContent = this.accessor.changedLabel; - this._statusSpan.style.fontWeight = 'bold'; - this.metadataHeaderContainer.classList.add('modified'); - } else { - this._statusSpan.textContent = this.accessor.unChangedLabel; - } - - const cellToolbarContainer = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-toolbar')); - this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { - actionViewItemProvider: action => { - if (action instanceof MenuItemAction) { - const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); - return item; - } - - return undefined; - } - }); - this._register(this._toolbar); - this._toolbar.context = { - cell: this.cell - }; - - this._menu = this.menuService.createMenu(this.accessor.menuId, this.contextKeyService); - this._register(this._menu); - - if (metadataChanged) { - const actions: IAction[] = []; - createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); - this._toolbar.setActions(actions); - } - - this._register(this.notebookEditor.onMouseUp(e => { - if (!e.event.target) { - return; - } - - const target = e.event.target as HTMLElement; - - if (target.classList.contains('codicon-notebook-collapsed') || target.classList.contains('codicon-notebook-expanded')) { - const parent = target.parentElement as HTMLElement; - - if (!parent) { - return; - } - - if (!parent.classList.contains(this.accessor.prefix)) { - return; - } - - if (!parent.classList.contains('property-folding-indicator')) { - return; - } - - // folding icon - - const cellViewModel = e.target; - - if (cellViewModel === this.cell) { - const oldFoldingState = this.accessor.getFoldingState(this.cell); - this.accessor.updateFoldingState(this.cell, oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded); - this._updateFoldingIcon(); - this.accessor.updateInfoRendering(); - } - } - - return; - })); - - this._updateFoldingIcon(); - this.accessor.updateInfoRendering(); - } - - refresh() { - let metadataChanged = this.accessor.checkIfModified(this.cell); - if (metadataChanged) { - this._statusSpan.textContent = this.accessor.changedLabel; - this._statusSpan.style.fontWeight = 'bold'; - this.metadataHeaderContainer.classList.add('modified'); - const actions: IAction[] = []; - createAndFillInActionBarActions(this._menu, undefined, actions); - this._toolbar.setActions(actions); - } else { - this._statusSpan.textContent = this.accessor.unChangedLabel; - this._statusSpan.style.fontWeight = 'normal'; - this._toolbar.setActions([]); - } - } - - private _updateFoldingIcon() { - if (this.accessor.getFoldingState(this.cell) === PropertyFoldingState.Collapsed) { - DOM.reset(this._foldingIndicator, ...renderCodicons(ThemeIcon.asCodiconLabel(collapsedIcon))); - } else { - DOM.reset(this._foldingIndicator, ...renderCodicons(ThemeIcon.asCodiconLabel(expandedIcon))); - } - } -} - -abstract class AbstractCellRenderer extends Disposable { - protected _metadataHeaderContainer!: HTMLElement; - protected _metadataHeader!: PropertyHeader; - protected _metadataInfoContainer!: HTMLElement; - protected _metadataEditorContainer?: HTMLElement; - protected _metadataEditorDisposeStore!: DisposableStore; - protected _metadataEditor?: CodeEditorWidget | DiffEditorWidget; - - protected _outputHeaderContainer!: HTMLElement; - protected _outputHeader!: PropertyHeader; - protected _outputInfoContainer!: HTMLElement; - protected _outputEditorContainer?: HTMLElement; - protected _outputEditorDisposeStore!: DisposableStore; - protected _outputEditor?: CodeEditorWidget | DiffEditorWidget; - - - protected _diffEditorContainer!: HTMLElement; - protected _diagonalFill?: HTMLElement; - protected _layoutInfo!: { - editorHeight: number; - editorMargin: number; - metadataStatusHeight: number; - metadataHeight: number; - outputStatusHeight: number; - outputHeight: number; - bodyMargin: number; - }; - protected _isDisposed: boolean; - - constructor( - readonly notebookEditor: INotebookTextDiffEditor, - readonly cell: CellDiffViewModel, - readonly templateData: CellDiffRenderTemplate, - readonly style: 'left' | 'right' | 'full', - protected readonly instantiationService: IInstantiationService, - protected readonly modeService: IModeService, - protected readonly modelService: IModelService, - protected readonly contextMenuService: IContextMenuService, - protected readonly keybindingService: IKeybindingService, - protected readonly notificationService: INotificationService, - protected readonly menuService: IMenuService, - protected readonly contextKeyService: IContextKeyService - - - ) { - super(); - // init - this._isDisposed = false; - this._layoutInfo = { - editorHeight: 0, - editorMargin: 0, - metadataHeight: 0, - metadataStatusHeight: 25, - outputHeight: 0, - outputStatusHeight: 25, - bodyMargin: 32 - }; - this._metadataEditorDisposeStore = new DisposableStore(); - this._outputEditorDisposeStore = new DisposableStore(); - this._register(this._metadataEditorDisposeStore); - this.initData(); - this.buildBody(templateData.container); - this._register(cell.onDidLayoutChange(e => this.onDidLayoutChange(e))); - } - - buildBody(container: HTMLElement) { - const body = DOM.$('.cell-body'); - DOM.append(container, body); - this._diffEditorContainer = DOM.$('.cell-diff-editor-container'); - switch (this.style) { - case 'left': - body.classList.add('left'); - break; - case 'right': - body.classList.add('right'); - break; - default: - body.classList.add('full'); - break; - } - - DOM.append(body, this._diffEditorContainer); - this._diagonalFill = DOM.append(body, DOM.$('.diagonal-fill')); - this.styleContainer(this._diffEditorContainer); - const sourceContainer = DOM.append(this._diffEditorContainer, DOM.$('.source-container')); - this.buildSourceEditor(sourceContainer); - - this._metadataHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-header-container')); - this._metadataInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-info-container')); - - const checkIfModified = (cell: CellDiffViewModel) => { - return cell.type !== 'delete' && cell.type !== 'insert' && hash(this._getFormatedMetadataJSON(cell.original?.metadata || {}, cell.original?.language)) !== hash(this._getFormatedMetadataJSON(cell.modified?.metadata ?? {}, cell.modified?.language)); - }; - - if (checkIfModified(this.cell)) { - this.cell.metadataFoldingState = PropertyFoldingState.Expanded; - } - - this._metadataHeader = this.instantiationService.createInstance( - PropertyHeader, - this.cell, - this._metadataHeaderContainer, - this.notebookEditor, - { - updateInfoRendering: this.updateMetadataRendering.bind(this), - checkIfModified: (cell) => { - return checkIfModified(cell); - }, - getFoldingState: (cell) => { - return cell.metadataFoldingState; - }, - updateFoldingState: (cell, state) => { - cell.metadataFoldingState = state; - }, - unChangedLabel: 'Metadata', - changedLabel: 'Metadata changed', - prefix: 'metadata', - menuId: MenuId.NotebookDiffCellMetadataTitle - } - ); - this._register(this._metadataHeader); - this._metadataHeader.buildHeader(); - - if (this.notebookEditor.textModel?.transientOptions.transientOutputs) { - this._layoutInfo.outputHeight = 0; - this._layoutInfo.outputStatusHeight = 0; - this.layout({}); - return; - } - - this._outputHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-header-container')); - this._outputInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-info-container')); - - const checkIfOutputsModified = (cell: CellDiffViewModel) => { - return cell.type !== 'delete' && cell.type !== 'insert' && !this.notebookEditor.textModel!.transientOptions.transientOutputs && cell.type === 'modified' && hash(cell.original?.outputs ?? []) !== hash(cell.modified?.outputs ?? []); - }; - - if (checkIfOutputsModified(this.cell)) { - this.cell.outputFoldingState = PropertyFoldingState.Expanded; - } - - this._outputHeader = this.instantiationService.createInstance( - PropertyHeader, - this.cell, - this._outputHeaderContainer, - this.notebookEditor, - { - updateInfoRendering: this.updateOutputRendering.bind(this), - checkIfModified: (cell) => { - return checkIfOutputsModified(cell); - }, - getFoldingState: (cell) => { - return cell.outputFoldingState; - }, - updateFoldingState: (cell, state) => { - cell.outputFoldingState = state; - }, - unChangedLabel: 'Outputs', - changedLabel: 'Outputs changed', - prefix: 'output', - menuId: MenuId.NotebookDiffCellOutputsTitle - } - ); - this._register(this._outputHeader); - this._outputHeader.buildHeader(); - } - - updateMetadataRendering() { - if (this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { - // we should expand the metadata editor - this._metadataInfoContainer.style.display = 'block'; - - if (!this._metadataEditorContainer || !this._metadataEditor) { - // create editor - this._metadataEditorContainer = DOM.append(this._metadataInfoContainer, DOM.$('.metadata-editor-container')); - this._buildMetadataEditor(); - } else { - this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); - this.layout({ metadataEditor: true }); - } - } else { - // we should collapse the metadata editor - this._metadataInfoContainer.style.display = 'none'; - this._metadataEditorDisposeStore.clear(); - this._layoutInfo.metadataHeight = 0; - this.layout({}); - } - } - - updateOutputRendering() { - if (this.cell.outputFoldingState === PropertyFoldingState.Expanded) { - this._outputInfoContainer.style.display = 'block'; - - if (!this._outputEditorContainer || !this._outputEditor) { - // create editor - this._outputEditorContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-editor-container')); - this._buildOutputEditor(); - } else { - this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); - this.layout({ outputEditor: true }); - } - } else { - this._outputInfoContainer.style.display = 'none'; - this._outputEditorDisposeStore.clear(); - this._layoutInfo.outputHeight = 0; - this.layout({}); - } - } - - protected _getFormatedMetadataJSON(metadata: NotebookCellMetadata, language?: string) { - let filteredMetadata: { [key: string]: any } = {}; - - if (this.notebookEditor.textModel) { - const transientMetadata = this.notebookEditor.textModel!.transientOptions.transientMetadata; - - const keys = new Set([...Object.keys(metadata)]); - for (let key of keys) { - if (!(transientMetadata[key as keyof NotebookCellMetadata]) - ) { - filteredMetadata[key] = metadata[key as keyof NotebookCellMetadata]; - } - } - } else { - filteredMetadata = metadata; - } - - const content = JSON.stringify({ - language, - ...filteredMetadata - }); - - const edits = format(content, undefined, {}); - const metadataSource = applyEdits(content, edits); - - return metadataSource; - } - - private _applySanitizedMetadataChanges(currentMetadata: NotebookCellMetadata, newMetadata: any) { - let result: { [key: string]: any } = {}; - let newLangauge: string | undefined = undefined; - try { - const newMetadataObj = JSON.parse(newMetadata); - const keys = new Set([...Object.keys(newMetadataObj)]); - for (let key of keys) { - switch (key as keyof NotebookCellMetadata) { - case 'breakpointMargin': - case 'editable': - case 'hasExecutionOrder': - case 'inputCollapsed': - case 'outputCollapsed': - case 'runnable': - // boolean - if (typeof newMetadataObj[key] === 'boolean') { - result[key] = newMetadataObj[key]; - } else { - result[key] = currentMetadata[key as keyof NotebookCellMetadata]; - } - break; - - case 'executionOrder': - case 'lastRunDuration': - // number - if (typeof newMetadataObj[key] === 'number') { - result[key] = newMetadataObj[key]; - } else { - result[key] = currentMetadata[key as keyof NotebookCellMetadata]; - } - break; - case 'runState': - // enum - if (typeof newMetadataObj[key] === 'number' && [1, 2, 3, 4].indexOf(newMetadataObj[key]) >= 0) { - result[key] = newMetadataObj[key]; - } else { - result[key] = currentMetadata[key as keyof NotebookCellMetadata]; - } - break; - case 'statusMessage': - // string - if (typeof newMetadataObj[key] === 'string') { - result[key] = newMetadataObj[key]; - } else { - result[key] = currentMetadata[key as keyof NotebookCellMetadata]; - } - break; - default: - if (key === 'language') { - newLangauge = newMetadataObj[key]; - } - result[key] = newMetadataObj[key]; - break; - } - } - - if (newLangauge !== undefined && newLangauge !== this.cell.modified!.language) { - const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!); - this.notebookEditor.textModel!.applyEdits( - this.notebookEditor.textModel!.versionId, - [{ editType: CellEditType.CellLanguage, index, language: newLangauge }], - true, - undefined, - () => undefined, - undefined - ); - } - - const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!); - - if (index < 0) { - return; - } - - this.notebookEditor.textModel!.applyEdits(this.notebookEditor.textModel!.versionId, [ - { editType: CellEditType.Metadata, index, metadata: result } - ], true, undefined, () => undefined, undefined); - } catch { - } - } - - private _buildMetadataEditor() { - if (this.cell.type === 'modified' || this.cell.type === 'unchanged') { - const originalMetadataSource = this._getFormatedMetadataJSON(this.cell.original?.metadata || {}, this.cell.original?.language); - const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language); - this._metadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._metadataEditorContainer!, { - ...fixedDiffEditorOptions, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - readOnly: false, - originalEditable: false, - ignoreTrimWhitespace: false - }); - this._register(this._metadataEditor); - - this._metadataEditorContainer?.classList.add('diff'); - - const mode = this.modeService.create('json'); - const originalMetadataModel = this.modelService.createModel(originalMetadataSource, mode, CellUri.generateCellMetadataUri(this.cell.original!.uri, this.cell.original!.handle), false); - const modifiedMetadataModel = this.modelService.createModel(modifiedMetadataSource, mode, CellUri.generateCellMetadataUri(this.cell.modified!.uri, this.cell.modified!.handle), false); - this._metadataEditor.setModel({ - original: originalMetadataModel, - modified: modifiedMetadataModel - }); - - this._register(originalMetadataModel); - this._register(modifiedMetadataModel); - - this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); - this.layout({ metadataEditor: true }); - - this._register(this._metadataEditor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { - this._layoutInfo.metadataHeight = e.contentHeight; - this.layout({ metadataEditor: true }); - } - })); - - let respondingToContentChange = false; - - this._register(modifiedMetadataModel.onDidChangeContent(() => { - respondingToContentChange = true; - const value = modifiedMetadataModel.getValue(); - this._applySanitizedMetadataChanges(this.cell.modified!.metadata, value); - this._metadataHeader.refresh(); - respondingToContentChange = false; - })); - - this._register(this.cell.modified!.onDidChangeMetadata(() => { - if (respondingToContentChange) { - return; - } - - const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language); - modifiedMetadataModel.setValue(modifiedMetadataSource); - })); - - return; - } - - this._metadataEditor = this.instantiationService.createInstance(CodeEditorWidget, this._metadataEditorContainer!, { - ...fixedEditorOptions, - dimension: { - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), - height: 0 - }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - readOnly: false - }, {}); - this._register(this._metadataEditor); - - const mode = this.modeService.create('jsonc'); - const originalMetadataSource = this._getFormatedMetadataJSON( - this.cell.type === 'insert' - ? this.cell.modified!.metadata || {} - : this.cell.original!.metadata || {}); - const uri = this.cell.type === 'insert' - ? this.cell.modified!.uri - : this.cell.original!.uri; - const handle = this.cell.type === 'insert' - ? this.cell.modified!.handle - : this.cell.original!.handle; - - const modelUri = CellUri.generateCellMetadataUri(uri, handle); - const metadataModel = this.modelService.createModel(originalMetadataSource, mode, modelUri, false); - this._metadataEditor.setModel(metadataModel); - this._register(metadataModel); - - this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); - this.layout({ metadataEditor: true }); - - this._register(this._metadataEditor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { - this._layoutInfo.metadataHeight = e.contentHeight; - this.layout({ metadataEditor: true }); - } - })); - } - - private _getFormatedOutputJSON(outputs: any[]) { - const content = JSON.stringify(outputs); - - const edits = format(content, undefined, {}); - const source = applyEdits(content, edits); - - return source; - } - - private _buildOutputEditor() { - if ((this.cell.type === 'modified' || this.cell.type === 'unchanged') && !this.notebookEditor.textModel!.transientOptions.transientOutputs) { - const originalOutputsSource = this._getFormatedOutputJSON(this.cell.original?.outputs || []); - const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); - if (originalOutputsSource !== modifiedOutputsSource) { - this._outputEditor = this.instantiationService.createInstance(DiffEditorWidget, this._outputEditorContainer!, { - ...fixedDiffEditorOptions, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - readOnly: true, - ignoreTrimWhitespace: false - }); - this._register(this._outputEditor); - - this._outputEditorContainer?.classList.add('diff'); - - const mode = this.modeService.create('json'); - const originalModel = this.modelService.createModel(originalOutputsSource, mode, undefined, true); - const modifiedModel = this.modelService.createModel(modifiedOutputsSource, mode, undefined, true); - this._outputEditor.setModel({ - original: originalModel, - modified: modifiedModel - }); - - this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); - this.layout({ outputEditor: true }); - - this._register(this._outputEditor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { - this._layoutInfo.outputHeight = e.contentHeight; - this.layout({ outputEditor: true }); - } - })); - - this._register(this.cell.modified!.onDidChangeOutputs(() => { - const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); - modifiedModel.setValue(modifiedOutputsSource); - this._outputHeader.refresh(); - })); - - return; - } - } - - this._outputEditor = this.instantiationService.createInstance(CodeEditorWidget, this._outputEditorContainer!, { - ...fixedEditorOptions, - dimension: { - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), - height: 0 - }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() - }, {}); - this._register(this._outputEditor); - - const mode = this.modeService.create('json'); - const originaloutputSource = this._getFormatedOutputJSON( - this.notebookEditor.textModel!.transientOptions.transientOutputs - ? [] - : this.cell.type === 'insert' - ? this.cell.modified!.outputs || [] - : this.cell.original!.outputs || []); - const outputModel = this.modelService.createModel(originaloutputSource, mode, undefined, true); - this._outputEditor.setModel(outputModel); - - this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); - this.layout({ outputEditor: true }); - - this._register(this._outputEditor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { - this._layoutInfo.outputHeight = e.contentHeight; - this.layout({ outputEditor: true }); - } - })); - } - - protected layoutNotebookCell() { - this.notebookEditor.layoutNotebookCell( - this.cell, - this._layoutInfo.editorHeight - + this._layoutInfo.editorMargin - + this._layoutInfo.metadataHeight - + this._layoutInfo.metadataStatusHeight - + this._layoutInfo.outputHeight - + this._layoutInfo.outputStatusHeight - + this._layoutInfo.bodyMargin - ); - } - - dispose() { - this._isDisposed = true; - super.dispose(); - } - - abstract initData(): void; - abstract styleContainer(container: HTMLElement): void; - abstract buildSourceEditor(sourceContainer: HTMLElement): void; - abstract onDidLayoutChange(event: CellDiffViewModelLayoutChangeEvent): void; - abstract layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }): void; -} - -export class DeletedCell extends AbstractCellRenderer { - private _editor!: CodeEditorWidget; - constructor( - readonly notebookEditor: INotebookTextDiffEditor, - readonly cell: CellDiffViewModel, - readonly templateData: CellDiffRenderTemplate, - @IModeService readonly modeService: IModeService, - @IModelService readonly modelService: IModelService, - @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IContextMenuService protected readonly contextMenuService: IContextMenuService, - @IKeybindingService protected readonly keybindingService: IKeybindingService, - @INotificationService protected readonly notificationService: INotificationService, - @IMenuService protected readonly menuService: IMenuService, - @IContextKeyService protected readonly contextKeyService: IContextKeyService, - - - ) { - super(notebookEditor, cell, templateData, 'left', instantiationService, modeService, modelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); - } - - initData(): void { - } - - styleContainer(container: HTMLElement) { - container.classList.add('removed'); - } - - buildSourceEditor(sourceContainer: HTMLElement): void { - const originalCell = this.cell.original!; - const lineCount = originalCell.textBuffer.getLineCount(); - const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; - - const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); - - this._editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, { - ...fixedEditorOptions, - dimension: { - width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, - height: editorHeight - }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() - }, {}); - this._register(this._editor); - - this._layoutInfo.editorHeight = editorHeight; - - this._register(this._editor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this._layoutInfo.editorHeight !== e.contentHeight) { - this._layoutInfo.editorHeight = e.contentHeight; - this.layout({ editorHeight: true }); - } - })); - - originalCell.resolveTextModelRef().then(ref => { - if (this._isDisposed) { - return; - } - - this._register(ref); - - const textModel = ref.object.textEditorModel; - this._editor.setModel(textModel); - this._layoutInfo.editorHeight = this._editor.getContentHeight(); - this.layout({ editorHeight: true }); - }); - - } - - onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { - if (e.outerWidth !== undefined) { - this.layout({ outerWidth: true }); - } - } - layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { - DOM.scheduleAtNextAnimationFrame(() => { - if (state.editorHeight || state.outerWidth) { - this._editor.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), - height: this._layoutInfo.editorHeight - }); - } - - if (state.metadataEditor || state.outerWidth) { - this._metadataEditor?.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), - height: this._layoutInfo.metadataHeight - }); - } - - if (state.outputEditor || state.outerWidth) { - this._outputEditor?.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), - height: this._layoutInfo.outputHeight - }); - } - - this.layoutNotebookCell(); - }); - } -} - -export class InsertCell extends AbstractCellRenderer { - private _editor!: CodeEditorWidget; - constructor( - readonly notebookEditor: INotebookTextDiffEditor, - readonly cell: CellDiffViewModel, - readonly templateData: CellDiffRenderTemplate, - @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IModeService readonly modeService: IModeService, - @IModelService readonly modelService: IModelService, - @IContextMenuService protected readonly contextMenuService: IContextMenuService, - @IKeybindingService protected readonly keybindingService: IKeybindingService, - @INotificationService protected readonly notificationService: INotificationService, - @IMenuService protected readonly menuService: IMenuService, - @IContextKeyService protected readonly contextKeyService: IContextKeyService, - ) { - super(notebookEditor, cell, templateData, 'right', instantiationService, modeService, modelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); - } - - initData(): void { - } - - styleContainer(container: HTMLElement): void { - container.classList.add('inserted'); - } - - buildSourceEditor(sourceContainer: HTMLElement): void { - const modifiedCell = this.cell.modified!; - const lineCount = modifiedCell.textBuffer.getLineCount(); - const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; - const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); - - this._editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, { - ...fixedEditorOptions, - dimension: { - width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, - height: editorHeight - }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - readOnly: false - }, {}); - this._register(this._editor); - - this._layoutInfo.editorHeight = editorHeight; - - this._register(this._editor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this._layoutInfo.editorHeight !== e.contentHeight) { - this._layoutInfo.editorHeight = e.contentHeight; - this.layout({ editorHeight: true }); - } - })); - - modifiedCell.resolveTextModelRef().then(ref => { - if (this._isDisposed) { - return; - } - - this._register(ref); - - const textModel = ref.object.textEditorModel; - this._editor.setModel(textModel); - this._layoutInfo.editorHeight = this._editor.getContentHeight(); - this.layout({ editorHeight: true }); - }); - } - - onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { - if (e.outerWidth !== undefined) { - this.layout({ outerWidth: true }); - } - } - - layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { - DOM.scheduleAtNextAnimationFrame(() => { - if (state.editorHeight || state.outerWidth) { - this._editor.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), - height: this._layoutInfo.editorHeight - }); - } - - if (state.metadataEditor || state.outerWidth) { - this._metadataEditor?.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), - height: this._layoutInfo.metadataHeight - }); - } - - if (state.outputEditor || state.outerWidth) { - this._outputEditor?.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), - height: this._layoutInfo.outputHeight - }); - } - - this.layoutNotebookCell(); - }); - } -} - -export class ModifiedCell extends AbstractCellRenderer { - private _editor?: DiffEditorWidget; - private _editorContainer!: HTMLElement; - private _inputToolbarContainer!: HTMLElement; - protected _toolbar!: ToolBar; - protected _menu!: IMenu; - - constructor( - readonly notebookEditor: INotebookTextDiffEditor, - readonly cell: CellDiffViewModel, - readonly templateData: CellDiffRenderTemplate, - @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IModeService readonly modeService: IModeService, - @IModelService readonly modelService: IModelService, - @IContextMenuService protected readonly contextMenuService: IContextMenuService, - @IKeybindingService protected readonly keybindingService: IKeybindingService, - @INotificationService protected readonly notificationService: INotificationService, - @IMenuService protected readonly menuService: IMenuService, - @IContextKeyService protected readonly contextKeyService: IContextKeyService - ) { - super(notebookEditor, cell, templateData, 'full', instantiationService, modeService, modelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); - } - - initData(): void { - } - - styleContainer(container: HTMLElement): void { - } - - buildSourceEditor(sourceContainer: HTMLElement): void { - const modifiedCell = this.cell.modified!; - const lineCount = modifiedCell.textBuffer.getLineCount(); - const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; - this._editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); - - this._editor = this.instantiationService.createInstance(DiffEditorWidget, this._editorContainer, { - ...fixedDiffEditorOptions, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - originalEditable: false, - ignoreTrimWhitespace: false - }); - this._register(this._editor); - this._editorContainer.classList.add('diff'); - - this._editor.layout({ - width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN, - height: editorHeight - }); - - this._editorContainer.style.height = `${editorHeight}px`; - - this._register(this._editor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this._layoutInfo.editorHeight !== e.contentHeight) { - this._layoutInfo.editorHeight = e.contentHeight; - this.layout({ editorHeight: true }); - } - })); - - this._initializeSourceDiffEditor(); - - this._inputToolbarContainer = DOM.append(sourceContainer, DOM.$('.editor-input-toolbar-container')); - const cellToolbarContainer = DOM.append(this._inputToolbarContainer, DOM.$('div.property-toolbar')); - this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { - actionViewItemProvider: action => { - if (action instanceof MenuItemAction) { - const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); - return item; - } - - return undefined; - } - }); - - this._toolbar.context = { - cell: this.cell - }; - - this._menu = this.menuService.createMenu(MenuId.NotebookDiffCellInputTitle, this.contextKeyService); - this._register(this._menu); - const actions: IAction[] = []; - createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); - this._toolbar.setActions(actions); - - if (this.cell.modified!.getValue() !== this.cell.original!.getValue()) { - this._inputToolbarContainer.style.display = 'block'; - } else { - this._inputToolbarContainer.style.display = 'none'; - } - - this._register(this.cell.modified!.onDidChangeContent(() => { - if (this.cell.modified!.getValue() !== this.cell.original!.getValue()) { - this._inputToolbarContainer.style.display = 'block'; - } else { - this._inputToolbarContainer.style.display = 'none'; - } - })); - } - - private async _initializeSourceDiffEditor() { - const originalCell = this.cell.original!; - const modifiedCell = this.cell.modified!; - - const originalRef = await originalCell.resolveTextModelRef(); - const modifiedRef = await modifiedCell.resolveTextModelRef(); - - if (this._isDisposed) { - return; - } - - const textModel = originalRef.object.textEditorModel; - const modifiedTextModel = modifiedRef.object.textEditorModel; - this._register(originalRef); - this._register(modifiedRef); - - this._editor!.setModel({ - original: textModel, - modified: modifiedTextModel - }); - - const contentHeight = this._editor!.getContentHeight(); - this._layoutInfo.editorHeight = contentHeight; - this.layout({ editorHeight: true }); - - } - - onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { - if (e.outerWidth !== undefined) { - this.layout({ outerWidth: true }); - } - } - - layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { - DOM.scheduleAtNextAnimationFrame(() => { - if (state.editorHeight || state.outerWidth) { - this._editorContainer.style.height = `${this._layoutInfo.editorHeight}px`; - this._editor!.layout(); - } - - if (state.metadataEditor || state.outerWidth) { - if (this._metadataEditorContainer) { - this._metadataEditorContainer.style.height = `${this._layoutInfo.metadataHeight}px`; - this._metadataEditor?.layout(); - } - } - - if (state.outputEditor || state.outerWidth) { - if (this._outputEditorContainer) { - this._outputEditorContainer.style.height = `${this._layoutInfo.outputHeight}px`; - this._outputEditor?.layout(); - } - } - - this.layoutNotebookCell(); - }); - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts deleted file mode 100644 index 55e25cee3..000000000 --- a/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts +++ /dev/null @@ -1,48 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; -import { Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/diff/common'; -import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; - -export enum PropertyFoldingState { - Expanded, - Collapsed -} - -export class CellDiffViewModel extends Disposable { - public metadataFoldingState: PropertyFoldingState; - public outputFoldingState: PropertyFoldingState; - private _layoutInfoEmitter = new Emitter(); - - onDidLayoutChange = this._layoutInfoEmitter.event; - - constructor( - readonly original: NotebookCellTextModel | undefined, - readonly modified: NotebookCellTextModel | undefined, - readonly type: 'unchanged' | 'insert' | 'delete' | 'modified', - readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher - ) { - super(); - this.metadataFoldingState = PropertyFoldingState.Collapsed; - this.outputFoldingState = PropertyFoldingState.Collapsed; - - this._register(this.editorEventDispatcher.onDidChangeLayout(e => { - this._layoutInfoEmitter.fire({ outerWidth: e.value.width }); - })); - } - - getComputedCellContainerWidth(layoutInfo: NotebookLayoutInfo, diffEditor: boolean, fullWidth: boolean) { - if (fullWidth) { - return layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0) - 2; - } - - return (layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)) / 2 - 18 - 2; - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/common.ts b/src/vs/workbench/contrib/notebook/browser/diff/common.ts deleted file mode 100644 index 505718cea..000000000 --- a/src/vs/workbench/contrib/notebook/browser/diff/common.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; -import { Event } from 'vs/base/common/event'; -import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; - -export interface INotebookTextDiffEditor { - readonly textModel?: NotebookTextModel; - onMouseUp: Event<{ readonly event: MouseEvent; readonly target: CellDiffViewModel; }>; - getOverflowContainerDomNode(): HTMLElement; - getLayoutInfo(): NotebookLayoutInfo; - layoutNotebookCell(cell: CellDiffViewModel, height: number): void; -} - -export interface CellDiffRenderTemplate { - readonly container: HTMLElement; - readonly elementDisposables: DisposableStore; -} - -export interface CellDiffViewModelLayoutChangeEvent { - font?: BareFontInfo; - outerWidth?: number; -} - -export const DIFF_CELL_MARGIN = 16; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts new file mode 100644 index 000000000..e2f4b6cd8 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -0,0 +1,1463 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { DiffElementViewModelBase, getFormatedMetadataJSON, OUTPUT_EDITOR_HEIGHT_MAGIC, PropertyFoldingState, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DiffSide, DIFF_CELL_MARGIN, INotebookTextDiffEditor, NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { CellEditType, CellUri, IProcessedOutput, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IAction } from 'vs/base/common/actions'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { Delayer } from 'vs/base/common/async'; +import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; +import { getEditorTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { collapsedIcon, expandedIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { OutputContainer } from 'vs/workbench/contrib/notebook/browser/diff/diffElementOutputs'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; +import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { AccessibilityHelpController } from 'vs/workbench/contrib/codeEditor/browser/accessibility/accessibility'; +import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; +import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; +import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/tabCompletion'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import * as editorCommon from 'vs/editor/common/editorCommon'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; + +export const fixedEditorOptions: IEditorOptions = { + padding: { + top: 12, + bottom: 12 + }, + scrollBeyondLastLine: false, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + vertical: 'hidden', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false, + alwaysConsumeMouseWheel: false, + }, + renderLineHighlightOnlyWhenFocus: true, + overviewRulerLanes: 0, + overviewRulerBorder: false, + selectOnLineNumbers: false, + wordWrap: 'off', + lineNumbers: 'off', + lineDecorationsWidth: 0, + glyphMargin: false, + fixedOverflowWidgets: true, + minimap: { enabled: false }, + renderValidationDecorations: 'on', + renderLineHighlight: 'none', + readOnly: true +}; + +export function getOptimizedNestedCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { + return { + isSimpleWidget: false, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + MenuPreventer.ID, + SelectionClipboardContributionID, + ContextMenuController.ID, + SuggestController.ID, + SnippetController2.ID, + TabCompletionController.ID, + AccessibilityHelpController.ID + ]) + }; +} + +export const fixedDiffEditorOptions: IDiffEditorOptions = { + ...fixedEditorOptions, + glyphMargin: true, + enableSplitViewResizing: false, + renderIndicators: false, + readOnly: false, + isInEmbeddedEditor: true, + renderOverviewRuler: false +}; + +class PropertyHeader extends Disposable { + protected _foldingIndicator!: HTMLElement; + protected _statusSpan!: HTMLElement; + protected _toolbar!: ToolBar; + protected _menu!: IMenu; + protected _propertyExpanded?: IContextKey; + + constructor( + readonly cell: DiffElementViewModelBase, + readonly propertyHeaderContainer: HTMLElement, + readonly notebookEditor: INotebookTextDiffEditor, + readonly accessor: { + updateInfoRendering: (renderOutput: boolean) => void; + checkIfModified: (cell: DiffElementViewModelBase) => boolean; + getFoldingState: (cell: DiffElementViewModelBase) => PropertyFoldingState; + updateFoldingState: (cell: DiffElementViewModelBase, newState: PropertyFoldingState) => void; + unChangedLabel: string; + changedLabel: string; + prefix: string; + menuId: MenuId; + }, + @IContextMenuService readonly contextMenuService: IContextMenuService, + @IKeybindingService readonly keybindingService: IKeybindingService, + @INotificationService readonly notificationService: INotificationService, + @IMenuService readonly menuService: IMenuService, + @IContextKeyService readonly contextKeyService: IContextKeyService + ) { + super(); + } + + buildHeader(): void { + let metadataChanged = this.accessor.checkIfModified(this.cell); + this._foldingIndicator = DOM.append(this.propertyHeaderContainer, DOM.$('.property-folding-indicator')); + this._foldingIndicator.classList.add(this.accessor.prefix); + this._updateFoldingIcon(); + const metadataStatus = DOM.append(this.propertyHeaderContainer, DOM.$('div.property-status')); + this._statusSpan = DOM.append(metadataStatus, DOM.$('span')); + + if (metadataChanged) { + this._statusSpan.textContent = this.accessor.changedLabel; + this._statusSpan.style.fontWeight = 'bold'; + this.propertyHeaderContainer.classList.add('modified'); + } else { + this._statusSpan.textContent = this.accessor.unChangedLabel; + } + + const cellToolbarContainer = DOM.append(this.propertyHeaderContainer, DOM.$('div.property-toolbar')); + this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { + actionViewItemProvider: action => { + if (action instanceof MenuItemAction) { + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); + return item; + } + + return undefined; + } + }); + this._register(this._toolbar); + this._toolbar.context = { + cell: this.cell + }; + + const scopedContextKeyService = this.contextKeyService.createScoped(cellToolbarContainer); + this._register(scopedContextKeyService); + const propertyChanged = NOTEBOOK_DIFF_CELL_PROPERTY.bindTo(scopedContextKeyService); + propertyChanged.set(metadataChanged); + this._propertyExpanded = NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED.bindTo(scopedContextKeyService); + + this._menu = this.menuService.createMenu(this.accessor.menuId, scopedContextKeyService); + this._register(this._menu); + + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); + this._toolbar.setActions(actions); + + this._register(this._menu.onDidChange(() => { + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); + this._toolbar.setActions(actions); + })); + + this._register(this.notebookEditor.onMouseUp(e => { + if (!e.event.target) { + return; + } + + const target = e.event.target as HTMLElement; + + if (target.classList.contains('codicon-notebook-collapsed') || target.classList.contains('codicon-notebook-expanded')) { + const parent = target.parentElement as HTMLElement; + + if (!parent) { + return; + } + + if (!parent.classList.contains(this.accessor.prefix)) { + return; + } + + if (!parent.classList.contains('property-folding-indicator')) { + return; + } + + // folding icon + + const cellViewModel = e.target; + + if (cellViewModel === this.cell) { + const oldFoldingState = this.accessor.getFoldingState(this.cell); + this.accessor.updateFoldingState(this.cell, oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded); + this._updateFoldingIcon(); + this.accessor.updateInfoRendering(this.cell.renderOutput); + } + } + + return; + })); + + this._updateFoldingIcon(); + this.accessor.updateInfoRendering(this.cell.renderOutput); + } + + refresh() { + let metadataChanged = this.accessor.checkIfModified(this.cell); + if (metadataChanged) { + this._statusSpan.textContent = this.accessor.changedLabel; + this._statusSpan.style.fontWeight = 'bold'; + this.propertyHeaderContainer.classList.add('modified'); + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, undefined, actions); + this._toolbar.setActions(actions); + } else { + this._statusSpan.textContent = this.accessor.unChangedLabel; + this._statusSpan.style.fontWeight = 'normal'; + this._toolbar.setActions([]); + } + } + + private _updateFoldingIcon() { + if (this.accessor.getFoldingState(this.cell) === PropertyFoldingState.Collapsed) { + DOM.reset(this._foldingIndicator, renderIcon(collapsedIcon)); + this._propertyExpanded?.set(false); + } else { + DOM.reset(this._foldingIndicator, renderIcon(expandedIcon)); + this._propertyExpanded?.set(true); + } + + } +} + +abstract class AbstractElementRenderer extends Disposable { + protected _metadataHeaderContainer!: HTMLElement; + protected _metadataHeader!: PropertyHeader; + protected _metadataInfoContainer!: HTMLElement; + protected _metadataEditorContainer?: HTMLElement; + protected _metadataEditorDisposeStore!: DisposableStore; + protected _metadataEditor?: CodeEditorWidget | DiffEditorWidget; + + protected _outputHeaderContainer!: HTMLElement; + protected _outputHeader!: PropertyHeader; + protected _outputInfoContainer!: HTMLElement; + protected _outputEditorContainer?: HTMLElement; + protected _outputViewContainer?: HTMLElement; + protected _outputLeftContainer?: HTMLElement; + protected _outputRightContainer?: HTMLElement; + protected _outputEmptyElement?: HTMLElement; + protected _outputLeftView?: OutputContainer; + protected _outputRightView?: OutputContainer; + protected _outputEditorDisposeStore!: DisposableStore; + protected _outputEditor?: CodeEditorWidget | DiffEditorWidget; + + + protected _diffEditorContainer!: HTMLElement; + protected _diagonalFill?: HTMLElement; + protected _isDisposed: boolean; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: DiffElementViewModelBase, + readonly templateData: CellDiffSingleSideRenderTemplate | CellDiffSideBySideRenderTemplate, + readonly style: 'left' | 'right' | 'full', + protected readonly instantiationService: IInstantiationService, + protected readonly modeService: IModeService, + protected readonly modelService: IModelService, + protected readonly textModelService: ITextModelService, + protected readonly contextMenuService: IContextMenuService, + protected readonly keybindingService: IKeybindingService, + protected readonly notificationService: INotificationService, + protected readonly menuService: IMenuService, + protected readonly contextKeyService: IContextKeyService + + + ) { + super(); + // init + this._isDisposed = false; + this._metadataEditorDisposeStore = new DisposableStore(); + this._outputEditorDisposeStore = new DisposableStore(); + this._register(this._metadataEditorDisposeStore); + this._register(this._outputEditorDisposeStore); + this._register(cell.onDidLayoutChange(e => this.layout(e))); + this._register(cell.onDidLayoutChange(e => this.updateBorders())); + this.buildBody(); + + this._register(cell.onDidStateChange(() => { + this.updateOutputRendering(this.cell.renderOutput); + })); + } + + abstract buildBody(): void; + + updateMetadataRendering() { + if (this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + // we should expand the metadata editor + this._metadataInfoContainer.style.display = 'block'; + + if (!this._metadataEditorContainer || !this._metadataEditor) { + // create editor + this._metadataEditorContainer = DOM.append(this._metadataInfoContainer, DOM.$('.metadata-editor-container')); + this._buildMetadataEditor(); + } else { + this.cell.metadataHeight = this._metadataEditor.getContentHeight(); + } + } else { + // we should collapse the metadata editor + this._metadataInfoContainer.style.display = 'none'; + // this._metadataEditorDisposeStore.clear(); + this.cell.metadataHeight = 0; + } + } + + updateOutputRendering(renderRichOutput: boolean) { + if (this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this._outputInfoContainer.style.display = 'block'; + if (renderRichOutput) { + this._hideOutputsRaw(); + this._buildOutputRendererContainer(); + this._showOutputsRenderer(); + this._showOutputsEmptyView(); + } else { + this._hideOutputsRenderer(); + this._buildOutputRawContainer(); + this._showOutputsRaw(); + } + } else { + this._outputInfoContainer.style.display = 'none'; + + this._hideOutputsRaw(); + this._hideOutputsRenderer(); + this._hideOutputsEmptyView(); + } + } + + private _buildOutputRawContainer() { + if (!this._outputEditorContainer) { + this._outputEditorContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-editor-container')); + this._buildOutputEditor(); + } + } + + private _showOutputsRaw() { + if (this._outputEditorContainer) { + this._outputEditorContainer.style.display = 'block'; + this.cell.rawOutputHeight = this._outputEditor!.getContentHeight(); + } + } + + private _showOutputsEmptyView() { + this.cell.layoutChange(); + } + + private _hideOutputsRaw() { + if (this._outputEditorContainer) { + this._outputEditorContainer.style.display = 'none'; + this.cell.rawOutputHeight = 0; + } + } + + private _hideOutputsEmptyView() { + this.cell.layoutChange(); + } + + abstract _buildOutputRendererContainer(): void; + abstract _hideOutputsRenderer(): void; + abstract _showOutputsRenderer(): void; + + private _applySanitizedMetadataChanges(currentMetadata: NotebookCellMetadata, newMetadata: any) { + let result: { [key: string]: any } = {}; + let newLangauge: string | undefined = undefined; + try { + const newMetadataObj = JSON.parse(newMetadata); + const keys = new Set([...Object.keys(newMetadataObj)]); + for (let key of keys) { + switch (key as keyof NotebookCellMetadata) { + case 'breakpointMargin': + case 'editable': + case 'hasExecutionOrder': + case 'inputCollapsed': + case 'outputCollapsed': + case 'runnable': + // boolean + if (typeof newMetadataObj[key] === 'boolean') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + + case 'executionOrder': + case 'lastRunDuration': + // number + if (typeof newMetadataObj[key] === 'number') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + case 'runState': + // enum + if (typeof newMetadataObj[key] === 'number' && [1, 2, 3, 4].indexOf(newMetadataObj[key]) >= 0) { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + case 'statusMessage': + // string + if (typeof newMetadataObj[key] === 'string') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + default: + if (key === 'language') { + newLangauge = newMetadataObj[key]; + } + result[key] = newMetadataObj[key]; + break; + } + } + + if (newLangauge !== undefined && newLangauge !== this.cell.modified!.language) { + const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!.textModel); + this.notebookEditor.textModel!.applyEdits( + this.notebookEditor.textModel!.versionId, + [{ editType: CellEditType.CellLanguage, index, language: newLangauge }], + true, + undefined, + () => undefined, + undefined + ); + } + + const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!.textModel); + + if (index < 0) { + return; + } + + this.notebookEditor.textModel!.applyEdits(this.notebookEditor.textModel!.versionId, [ + { editType: CellEditType.Metadata, index, metadata: result } + ], true, undefined, () => undefined, undefined); + } catch { + } + } + + private async _buildMetadataEditor() { + this._metadataEditorDisposeStore.clear(); + + if (this.cell instanceof SideBySideDiffElementViewModel) { + this._metadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._metadataEditorContainer!, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: false, + originalEditable: false, + ignoreTrimWhitespace: false, + automaticLayout: false, + dimension: { + height: this.cell.layoutInfo.metadataHeight, + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), true, true) + } + }, { + originalEditor: getOptimizedNestedCodeEditorWidgetOptions(), + modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() + }); + this.layout({ metadataHeight: true }); + this._metadataEditorDisposeStore.add(this._metadataEditor); + + this._metadataEditorContainer?.classList.add('diff'); + + const originalMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellMetadataUri(this.cell.originalDocument.uri, this.cell.original!.handle)); + const modifiedMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellMetadataUri(this.cell.modifiedDocument.uri, this.cell.modified!.handle)); + this._metadataEditor.setModel({ + original: originalMetadataModel.object.textEditorModel, + modified: modifiedMetadataModel.object.textEditorModel + }); + + this._metadataEditorDisposeStore.add(originalMetadataModel); + this._metadataEditorDisposeStore.add(modifiedMetadataModel); + + this.cell.metadataHeight = this._metadataEditor.getContentHeight(); + + this._metadataEditorDisposeStore.add(this._metadataEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + this.cell.metadataHeight = e.contentHeight; + } + })); + + let respondingToContentChange = false; + + this._metadataEditorDisposeStore.add(modifiedMetadataModel.object.textEditorModel.onDidChangeContent(() => { + respondingToContentChange = true; + const value = modifiedMetadataModel.object.textEditorModel.getValue(); + this._applySanitizedMetadataChanges(this.cell.modified!.metadata, value); + this._metadataHeader.refresh(); + respondingToContentChange = false; + })); + + this._metadataEditorDisposeStore.add(this.cell.modified!.textModel.onDidChangeMetadata(() => { + if (respondingToContentChange) { + return; + } + + const modifiedMetadataSource = getFormatedMetadataJSON(this.notebookEditor.textModel!, this.cell.modified?.metadata || {}, this.cell.modified?.language); + modifiedMetadataModel.object.textEditorModel.setValue(modifiedMetadataSource); + })); + + return; + } else { + this._metadataEditor = this.instantiationService.createInstance(CodeEditorWidget, this._metadataEditorContainer!, { + ...fixedEditorOptions, + dimension: { + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: this.cell.layoutInfo.metadataHeight + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: false + }, {}); + this.layout({ metadataHeight: true }); + this._metadataEditorDisposeStore.add(this._metadataEditor); + + const mode = this.modeService.create('jsonc'); + const originalMetadataSource = getFormatedMetadataJSON(this.notebookEditor.textModel!, + this.cell.type === 'insert' + ? this.cell.modified!.metadata || {} + : this.cell.original!.metadata || {}); + const uri = this.cell.type === 'insert' + ? this.cell.modified!.uri + : this.cell.original!.uri; + const handle = this.cell.type === 'insert' + ? this.cell.modified!.handle + : this.cell.original!.handle; + + const modelUri = CellUri.generateCellMetadataUri(uri, handle); + const metadataModel = this.modelService.createModel(originalMetadataSource, mode, modelUri, false); + this._metadataEditor.setModel(metadataModel); + this._metadataEditorDisposeStore.add(metadataModel); + + this.cell.metadataHeight = this._metadataEditor.getContentHeight(); + + this._metadataEditorDisposeStore.add(this._metadataEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + this.cell.metadataHeight = e.contentHeight; + } + })); + } + } + + private _getFormatedOutputJSON(outputs: IProcessedOutput[]) { + return JSON.stringify(outputs, undefined, '\t'); + } + + private _buildOutputEditor() { + this._outputEditorDisposeStore.clear(); + + if ((this.cell.type === 'modified' || this.cell.type === 'unchanged') && !this.notebookEditor.textModel!.transientOptions.transientOutputs) { + const originalOutputsSource = this._getFormatedOutputJSON(this.cell.original?.outputs || []); + const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); + if (originalOutputsSource !== modifiedOutputsSource) { + const mode = this.modeService.create('json'); + const originalModel = this.modelService.createModel(originalOutputsSource, mode, undefined, true); + const modifiedModel = this.modelService.createModel(modifiedOutputsSource, mode, undefined, true); + this._outputEditorDisposeStore.add(originalModel); + this._outputEditorDisposeStore.add(modifiedModel); + + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const lineCount = Math.max(originalModel.getLineCount(), modifiedModel.getLineCount()); + this._outputEditor = this.instantiationService.createInstance(DiffEditorWidget, this._outputEditorContainer!, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: true, + ignoreTrimWhitespace: false, + automaticLayout: false, + dimension: { + height: Math.min(OUTPUT_EDITOR_HEIGHT_MAGIC, this.cell.layoutInfo.rawOutputHeight || lineHeight * lineCount), + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true) + } + }, { + originalEditor: getOptimizedNestedCodeEditorWidgetOptions(), + modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() + }); + this._outputEditorDisposeStore.add(this._outputEditor); + + this._outputEditorContainer?.classList.add('diff'); + + this._outputEditor.setModel({ + original: originalModel, + modified: modifiedModel + }); + this._outputEditor.restoreViewState(this.cell.getOutputEditorViewState() as editorCommon.IDiffEditorViewState); + + this.cell.rawOutputHeight = this._outputEditor.getContentHeight(); + + this._outputEditorDisposeStore.add(this._outputEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this.cell.rawOutputHeight = e.contentHeight; + } + })); + + this._outputEditorDisposeStore.add(this.cell.modified!.textModel.onDidChangeOutputs(() => { + const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); + modifiedModel.setValue(modifiedOutputsSource); + this._outputHeader.refresh(); + })); + + return; + } + } + + this._outputEditor = this.instantiationService.createInstance(CodeEditorWidget, this._outputEditorContainer!, { + ...fixedEditorOptions, + dimension: { + width: Math.min(OUTPUT_EDITOR_HEIGHT_MAGIC, this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, this.cell.type === 'unchanged' || this.cell.type === 'modified') - 32), + height: this.cell.layoutInfo.rawOutputHeight + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() + }, {}); + this._outputEditorDisposeStore.add(this._outputEditor); + + const mode = this.modeService.create('json'); + const originaloutputSource = this._getFormatedOutputJSON( + this.notebookEditor.textModel!.transientOptions.transientOutputs + ? [] + : this.cell.type === 'insert' + ? this.cell.modified!.outputs || [] + : this.cell.original!.outputs || []); + const outputModel = this.modelService.createModel(originaloutputSource, mode, undefined, true); + this._outputEditorDisposeStore.add(outputModel); + this._outputEditor.setModel(outputModel); + this._outputEditor.restoreViewState(this.cell.getOutputEditorViewState()); + + this.cell.rawOutputHeight = this._outputEditor.getContentHeight(); + + this._outputEditorDisposeStore.add(this._outputEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this.cell.rawOutputHeight = e.contentHeight; + } + })); + } + + protected layoutNotebookCell() { + this.notebookEditor.layoutNotebookCell( + this.cell, + this.cell.layoutInfo.totalHeight + ); + } + + updateBorders() { + this.templateData.leftBorder.style.height = `${this.cell.layoutInfo.totalHeight - 32}px`; + this.templateData.rightBorder.style.height = `${this.cell.layoutInfo.totalHeight - 32}px`; + this.templateData.bottomBorder.style.top = `${this.cell.layoutInfo.totalHeight - 32}px`; + } + + dispose() { + if (this._outputEditor) { + this.cell.saveOutputEditorViewState(this._outputEditor.saveViewState()); + } + + if (this._metadataEditor) { + this.cell.saveMetadataEditorViewState(this._metadataEditor.saveViewState()); + } + + this._metadataEditorDisposeStore.dispose(); + this._outputEditorDisposeStore.dispose(); + + this._isDisposed = true; + super.dispose(); + } + + abstract styleContainer(container: HTMLElement): void; + abstract updateSourceEditor(): void; + abstract layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, metadataHeight?: boolean, outputEditor?: boolean, outputView?: boolean }): void; +} + +abstract class SingleSideDiffElement extends AbstractElementRenderer { + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: SingleSideDiffElementViewModel, + readonly templateData: CellDiffSingleSideRenderTemplate, + readonly style: 'left' | 'right' | 'full', + protected readonly instantiationService: IInstantiationService, + protected readonly modeService: IModeService, + protected readonly modelService: IModelService, + protected readonly textModelService: ITextModelService, + protected readonly contextMenuService: IContextMenuService, + protected readonly keybindingService: IKeybindingService, + protected readonly notificationService: INotificationService, + protected readonly menuService: IMenuService, + protected readonly contextKeyService: IContextKeyService + + + ) { + super( + notebookEditor, + cell, + templateData, + style, + instantiationService, + modeService, + modelService, + textModelService, + contextMenuService, + keybindingService, + notificationService, + menuService, + contextKeyService + ); + } + + buildBody() { + const body = this.templateData.body; + this._diffEditorContainer = this.templateData.diffEditorContainer; + switch (this.style) { + case 'left': + body.classList.add('left'); + break; + case 'right': + body.classList.add('right'); + break; + default: + body.classList.add('full'); + break; + } + + this._diagonalFill = this.templateData.diagonalFill; + this.styleContainer(this._diffEditorContainer); + this.updateSourceEditor(); + + this._metadataHeaderContainer = this.templateData.metadataHeaderContainer; + this._metadataInfoContainer = this.templateData.metadataInfoContainer; + this._metadataHeaderContainer.innerText = ''; + this._metadataInfoContainer.innerText = ''; + + this._metadataHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._metadataHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateMetadataRendering.bind(this), + checkIfModified: (cell) => { + return cell.checkMetadataIfModified(); + }, + getFoldingState: (cell) => { + return cell.metadataFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.metadataFoldingState = state; + }, + unChangedLabel: 'Metadata', + changedLabel: 'Metadata changed', + prefix: 'metadata', + menuId: MenuId.NotebookDiffCellMetadataTitle + } + ); + this._register(this._metadataHeader); + this._metadataHeader.buildHeader(); + + if (this.notebookEditor.textModel?.transientOptions.transientOutputs) { + this.cell.rawOutputHeight = 0; + this.cell.outputStatusHeight = 0; + this.templateData.outputHeaderContainer.style.display = 'none'; + this.templateData.outputInfoContainer.style.display = 'none'; + return; + } else { + this.templateData.outputHeaderContainer.style.display = 'flex'; + this.templateData.outputInfoContainer.style.display = 'block'; + + } + + this._outputHeaderContainer = this.templateData.outputHeaderContainer; + this._outputInfoContainer = this.templateData.outputInfoContainer; + + this._outputHeaderContainer.innerText = ''; + this._outputInfoContainer.innerText = ''; + + this._outputHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._outputHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateOutputRendering.bind(this), + checkIfModified: (cell) => { + return cell.checkIfOutputsModified(); + }, + getFoldingState: (cell) => { + return cell.outputFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.outputFoldingState = state; + }, + unChangedLabel: 'Outputs', + changedLabel: 'Outputs changed', + prefix: 'output', + menuId: MenuId.NotebookDiffCellOutputsTitle + } + ); + this._register(this._outputHeader); + this._outputHeader.buildHeader(); + } +} +export class DeletedElement extends SingleSideDiffElement { + private _editor!: CodeEditorWidget; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: SingleSideDiffElementViewModel, + readonly templateData: CellDiffSingleSideRenderTemplate, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + @ITextModelService readonly textModelService: ITextModelService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IContextMenuService protected readonly contextMenuService: IContextMenuService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, + @INotificationService protected readonly notificationService: INotificationService, + @IMenuService protected readonly menuService: IMenuService, + @IContextKeyService protected readonly contextKeyService: IContextKeyService, + + + ) { + super(notebookEditor, cell, templateData, 'left', instantiationService, modeService, modelService, textModelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); + } + + styleContainer(container: HTMLElement) { + container.classList.remove('inserted'); + container.classList.add('removed'); + } + + updateSourceEditor(): void { + const originalCell = this.cell.original!; + const lineCount = originalCell.textModel.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; + + this._editor = this.templateData.sourceEditor; + this._editor.layout({ + width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, + height: editorHeight + }); + + this.cell.editorHeight = editorHeight; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) { + this.cell.editorHeight = e.contentHeight; + } + })); + + originalCell.textModel.resolveTextModelRef().then(ref => { + if (this._isDisposed) { + return; + } + + this._register(ref); + + const textModel = ref.object.textEditorModel; + this._editor.setModel(textModel); + this.cell.editorHeight = this._editor.getContentHeight(); + }); + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataHeight?: boolean, outputTotalHeight?: boolean }) { + DOM.scheduleAtNextAnimationFrame(() => { + if (state.editorHeight || state.outerWidth) { + this._editor.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.editorHeight + }); + } + + if (state.metadataHeight || state.outerWidth) { + this._metadataEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.metadataHeight + }); + } + + if (state.outputTotalHeight || state.outerWidth) { + this._outputEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.outputTotalHeight + }); + } + + if (this._diagonalFill) { + this._diagonalFill.style.height = `${this.cell.layoutInfo.totalHeight - 32}px`; + } + + this.layoutNotebookCell(); + }); + } + + _buildOutputRendererContainer() { + if (!this._outputViewContainer) { + this._outputViewContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-view-container')); + this._outputEmptyElement = DOM.append(this._outputViewContainer, DOM.$('.output-empty-view')); + const span = DOM.append(this._outputEmptyElement, DOM.$('span')); + span.innerText = 'No outputs to render'; + + if (this.cell.original!.outputs.length === 0) { + this._outputEmptyElement.style.display = 'block'; + } else { + this._outputEmptyElement.style.display = 'none'; + } + + this.cell.layoutChange(); + + this._outputLeftView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.original!, DiffSide.Original, this._outputViewContainer!); + this._register(this._outputLeftView); + this._outputLeftView.render(); + + const removedOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => { + if (e.cell.uri.toString() === this.cell.original!.uri.toString()) { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original!.id, ['nb-cellDeleted'], []); + removedOutputRenderListener.dispose(); + } + }); + + this._register(removedOutputRenderListener); + } + + this._outputViewContainer.style.display = 'block'; + } + + _decorate() { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original!.id, ['nb-cellDeleted'], []); + } + + _showOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'block'; + + this._outputLeftView?.showOutputs(); + this._decorate(); + } + } + + _hideOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'none'; + + this._outputLeftView?.hideOutputs(); + } + } + + dispose() { + if (this._editor) { + this.cell.saveSpirceEditorViewState(this._editor.saveViewState()); + } + + super.dispose(); + } +} + +export class InsertElement extends SingleSideDiffElement { + private _editor!: CodeEditorWidget; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: SingleSideDiffElementViewModel, + readonly templateData: CellDiffSingleSideRenderTemplate, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + @ITextModelService readonly textModelService: ITextModelService, + @IContextMenuService protected readonly contextMenuService: IContextMenuService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, + @INotificationService protected readonly notificationService: INotificationService, + @IMenuService protected readonly menuService: IMenuService, + @IContextKeyService protected readonly contextKeyService: IContextKeyService, + ) { + super(notebookEditor, cell, templateData, 'right', instantiationService, modeService, modelService, textModelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); + } + + styleContainer(container: HTMLElement): void { + container.classList.remove('removed'); + container.classList.add('inserted'); + } + + updateSourceEditor(): void { + const modifiedCell = this.cell.modified!; + const lineCount = modifiedCell.textModel.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; + + this._editor = this.templateData.sourceEditor; + this._editor.layout( + { + width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, + height: editorHeight + } + ); + this._editor.updateOptions({ readOnly: false }); + this.cell.editorHeight = editorHeight; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) { + this.cell.editorHeight = e.contentHeight; + } + })); + + modifiedCell.textModel.resolveTextModelRef().then(ref => { + if (this._isDisposed) { + return; + } + + this._register(ref); + + const textModel = ref.object.textEditorModel; + this._editor.setModel(textModel); + this._editor.restoreViewState(this.cell.getSourceEditorViewState() as editorCommon.ICodeEditorViewState); + this.cell.editorHeight = this._editor.getContentHeight(); + }); + } + + _buildOutputRendererContainer() { + if (!this._outputViewContainer) { + this._outputViewContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-view-container')); + this._outputEmptyElement = DOM.append(this._outputViewContainer, DOM.$('.output-empty-view')); + this._outputEmptyElement.innerText = 'No outputs to render'; + + if (this.cell.modified!.outputs.length === 0) { + this._outputEmptyElement.style.display = 'block'; + } else { + this._outputEmptyElement.style.display = 'none'; + } + + this.cell.layoutChange(); + + this._outputRightView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.modified!, DiffSide.Modified, this._outputViewContainer!); + this._register(this._outputRightView); + this._outputRightView.render(); + + const insertOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => { + if (e.cell.uri.toString() === this.cell.modified!.uri.toString()) { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified!.id, ['nb-cellAdded'], []); + insertOutputRenderListener.dispose(); + } + }); + this._register(insertOutputRenderListener); + } + + this._outputViewContainer.style.display = 'block'; + } + + _decorate() { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified!.id, ['nb-cellAdded'], []); + } + + _showOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'block'; + this._outputRightView?.showOutputs(); + this._decorate(); + } + } + + _hideOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'none'; + this._outputRightView?.hideOutputs(); + } + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataHeight?: boolean, outputTotalHeight?: boolean }) { + DOM.scheduleAtNextAnimationFrame(() => { + if (state.editorHeight || state.outerWidth) { + this._editor.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.editorHeight + }); + } + + if (state.metadataHeight || state.outerWidth) { + this._metadataEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: this.cell.layoutInfo.metadataHeight + }); + } + + if (state.outputTotalHeight || state.outerWidth) { + this._outputEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.outputTotalHeight + }); + } + + this.layoutNotebookCell(); + + if (this._diagonalFill) { + this._diagonalFill.style.height = `${this.cell.layoutInfo.editorHeight + this.cell.layoutInfo.editorMargin + this.cell.layoutInfo.metadataStatusHeight + this.cell.layoutInfo.metadataHeight + this.cell.layoutInfo.outputTotalHeight + this.cell.layoutInfo.outputStatusHeight}px`; + } + }); + } + + dispose() { + if (this._editor) { + this.cell.saveSpirceEditorViewState(this._editor.saveViewState()); + } + + super.dispose(); + } +} + +export class ModifiedElement extends AbstractElementRenderer { + private _editor?: DiffEditorWidget; + private _editorContainer!: HTMLElement; + private _inputToolbarContainer!: HTMLElement; + protected _toolbar!: ToolBar; + protected _menu!: IMenu; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: SideBySideDiffElementViewModel, + readonly templateData: CellDiffSideBySideRenderTemplate, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + @ITextModelService readonly textModelService: ITextModelService, + @IContextMenuService protected readonly contextMenuService: IContextMenuService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, + @INotificationService protected readonly notificationService: INotificationService, + @IMenuService protected readonly menuService: IMenuService, + @IContextKeyService protected readonly contextKeyService: IContextKeyService + ) { + super(notebookEditor, cell, templateData, 'full', instantiationService, modeService, modelService, textModelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); + } + + styleContainer(container: HTMLElement): void { + container.classList.remove('inserted', 'removed'); + } + + buildBody() { + const body = this.templateData.body; + this._diffEditorContainer = this.templateData.diffEditorContainer; + body.classList.remove('left', 'right', 'full'); + switch (this.style) { + case 'left': + body.classList.add('left'); + break; + case 'right': + body.classList.add('right'); + break; + default: + body.classList.add('full'); + break; + } + + this.styleContainer(this._diffEditorContainer); + this.updateSourceEditor(); + + this._metadataHeaderContainer = this.templateData.metadataHeaderContainer; + this._metadataInfoContainer = this.templateData.metadataInfoContainer; + + this._metadataHeaderContainer.innerText = ''; + this._metadataInfoContainer.innerText = ''; + + this._metadataHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._metadataHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateMetadataRendering.bind(this), + checkIfModified: (cell) => { + return cell.checkMetadataIfModified(); + }, + getFoldingState: (cell) => { + return cell.metadataFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.metadataFoldingState = state; + }, + unChangedLabel: 'Metadata', + changedLabel: 'Metadata changed', + prefix: 'metadata', + menuId: MenuId.NotebookDiffCellMetadataTitle + } + ); + this._register(this._metadataHeader); + this._metadataHeader.buildHeader(); + + if (this.notebookEditor.textModel?.transientOptions.transientOutputs) { + this.cell.rawOutputHeight = 0; + this.cell.outputStatusHeight = 0; + this.templateData.outputHeaderContainer.style.display = 'none'; + this.templateData.outputInfoContainer.style.display = 'none'; + return; + } else { + this.templateData.outputHeaderContainer.style.display = 'flex'; + this.templateData.outputInfoContainer.style.display = 'block'; + } + + this._outputHeaderContainer = this.templateData.outputHeaderContainer; + this._outputInfoContainer = this.templateData.outputInfoContainer; + this._outputHeaderContainer.innerText = ''; + this._outputInfoContainer.innerText = ''; + + if (this.cell.checkIfOutputsModified()) { + this._outputInfoContainer.classList.add('modified'); + } + + this._outputHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._outputHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateOutputRendering.bind(this), + checkIfModified: (cell) => { + return cell.checkIfOutputsModified(); + }, + getFoldingState: (cell) => { + return cell.outputFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.outputFoldingState = state; + }, + unChangedLabel: 'Outputs', + changedLabel: 'Outputs changed', + prefix: 'output', + menuId: MenuId.NotebookDiffCellOutputsTitle + } + ); + this._register(this._outputHeader); + this._outputHeader.buildHeader(); + } + + _buildOutputRendererContainer() { + if (!this._outputViewContainer) { + this._outputViewContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-view-container')); + this._outputEmptyElement = DOM.append(this._outputViewContainer, DOM.$('.output-empty-view')); + this._outputEmptyElement.innerText = 'No outputs to render'; + + if (!this.cell.checkIfOutputsModified() && this.cell.modified.outputs.length === 0) { + this._outputEmptyElement.style.display = 'block'; + } else { + this._outputEmptyElement.style.display = 'none'; + } + + this.cell.layoutChange(); + + this._register(this.cell.modified.textModel.onDidChangeOutputs(() => { + // currently we only allow outputs change to the modified cell + if (!this.cell.checkIfOutputsModified() && this.cell.modified.outputs.length === 0) { + this._outputEmptyElement!.style.display = 'block'; + } else { + this._outputEmptyElement!.style.display = 'none'; + } + })); + + this._outputLeftContainer = DOM.append(this._outputViewContainer!, DOM.$('.output-view-container-left')); + this._outputRightContainer = DOM.append(this._outputViewContainer!, DOM.$('.output-view-container-right')); + // We should use the original text model here + this._outputLeftView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.original!, DiffSide.Original, this._outputLeftContainer!); + this._outputLeftView.render(); + this._register(this._outputLeftView); + this._outputRightView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.modified!, DiffSide.Modified, this._outputRightContainer!); + this._outputRightView.render(); + this._register(this._outputRightView); + + const originalOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => { + if (e.cell.uri.toString() === this.cell.original.uri.toString()) { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original.id, ['nb-cellDeleted'], []); + originalOutputRenderListener.dispose(); + } + }); + + const modifiedOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => { + if (e.cell.uri.toString() === this.cell.modified.uri.toString()) { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified.id, ['nb-cellAdded'], []); + modifiedOutputRenderListener.dispose(); + } + }); + + this._register(originalOutputRenderListener); + this._register(modifiedOutputRenderListener); + + this._decorate(); + } + + this._outputViewContainer.style.display = 'block'; + } + + _decorate() { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original.id, ['nb-cellDeleted'], []); + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified.id, ['nb-cellAdded'], []); + } + + _showOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'block'; + + this._outputLeftView?.showOutputs(); + this._outputRightView?.showOutputs(); + this._decorate(); + } + } + + _hideOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'none'; + + this._outputLeftView?.hideOutputs(); + this._outputRightView?.hideOutputs(); + } + } + + updateSourceEditor(): void { + const modifiedCell = this.cell.modified!; + const lineCount = modifiedCell.textModel.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = this.cell.layoutInfo.editorHeight !== 0 ? this.cell.layoutInfo.editorHeight : lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; + this._editorContainer = this.templateData.editorContainer; + this._editor = this.templateData.sourceEditor; + + this._editorContainer.classList.add('diff'); + + this._editor.layout({ + width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN, + height: editorHeight + }); + + this._editorContainer.style.height = `${editorHeight}px`; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) { + this.cell.editorHeight = e.contentHeight; + } + })); + + this._initializeSourceDiffEditor(); + + this._inputToolbarContainer = this.templateData.inputToolbarContainer; + this._toolbar = this.templateData.toolbar; + + this._toolbar.context = { + cell: this.cell + }; + + this._menu = this.menuService.createMenu(MenuId.NotebookDiffCellInputTitle, this.contextKeyService); + this._register(this._menu); + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); + this._toolbar.setActions(actions); + + if (this.cell.modified!.textModel.getValue() !== this.cell.original!.textModel.getValue()) { + this._inputToolbarContainer.style.display = 'block'; + } else { + this._inputToolbarContainer.style.display = 'none'; + } + + this._register(this.cell.modified!.textModel.onDidChangeContent(() => { + if (this.cell.modified!.textModel.getValue() !== this.cell.original!.textModel.getValue()) { + this._inputToolbarContainer.style.display = 'block'; + } else { + this._inputToolbarContainer.style.display = 'none'; + } + })); + } + + private async _initializeSourceDiffEditor() { + const originalCell = this.cell.original!; + const modifiedCell = this.cell.modified!; + + const originalRef = await originalCell.textModel.resolveTextModelRef(); + const modifiedRef = await modifiedCell.textModel.resolveTextModelRef(); + + if (this._isDisposed) { + return; + } + + const textModel = originalRef.object.textEditorModel; + const modifiedTextModel = modifiedRef.object.textEditorModel; + this._register({ + dispose: () => { + const delayer = new Delayer(5000); + delayer.trigger(() => { + originalRef.dispose(); + delayer.dispose(); + }); + } + }); + this._register({ + dispose: () => { + const delayer = new Delayer(5000); + delayer.trigger(() => { + modifiedRef.dispose(); + delayer.dispose(); + }); + } + }); + + this._editor!.setModel({ + original: textModel, + modified: modifiedTextModel + }); + + this._editor!.restoreViewState(this.cell.getSourceEditorViewState() as editorCommon.IDiffEditorViewState); + + const contentHeight = this._editor!.getContentHeight(); + this.cell.editorHeight = contentHeight; + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataHeight?: boolean, outputTotalHeight?: boolean }) { + DOM.scheduleAtNextAnimationFrame(() => { + if (state.editorHeight) { + this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; + this._editor!.layout({ + width: this._editor!.getViewWidth(), + height: this.cell.layoutInfo.editorHeight + }); + } + + if (state.outerWidth) { + this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; + this._editor!.layout(); + } + + if (state.metadataHeight || state.outerWidth) { + if (this._metadataEditorContainer) { + this._metadataEditorContainer.style.height = `${this.cell.layoutInfo.metadataHeight}px`; + this._metadataEditor?.layout(); + } + } + + if (state.outputTotalHeight || state.outerWidth) { + if (this._outputEditorContainer) { + this._outputEditorContainer.style.height = `${this.cell.layoutInfo.outputTotalHeight}px`; + this._outputEditor?.layout(); + } + } + + + this.layoutNotebookCell(); + }); + } + + dispose() { + if (this._editor) { + this.cell.saveSpirceEditorViewState(this._editor.saveViewState()); + } + + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts new file mode 100644 index 000000000..74a0010fc --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts @@ -0,0 +1,361 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import * as nls from 'vs/nls'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { DiffElementViewModelBase, SideBySideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { DiffSide, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { ICellOutputViewModel, IDisplayOutputViewModel, IRenderOutput, outputHasDynamicHeight, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { BUILTIN_RENDERER_ID, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { mimetypeIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; + +interface IMimeTypeRenderer extends IQuickPickItem { + index: number; +} + +export class OutputElement extends Disposable { + readonly resizeListener = new DisposableStore(); + domNode!: HTMLElement; + renderResult?: IRenderOutput; + + constructor( + private _notebookEditor: INotebookTextDiffEditor, + private _notebookTextModel: NotebookTextModel, + private _notebookService: INotebookService, + private _quickInputService: IQuickInputService, + private _diffElementViewModel: DiffElementViewModelBase, + private _diffSide: DiffSide, + private _nestedCell: DiffNestedCellViewModel, + private _outputContainer: HTMLElement, + readonly output: ICellOutputViewModel + ) { + super(); + } + + render(index: number, beforeElement?: HTMLElement) { + const outputItemDiv = document.createElement('div'); + let result: IRenderOutput | undefined = undefined; + + if (this.output.isDisplayOutput()) { + const [mimeTypes, pick] = this.output.resolveMimeTypes(this._notebookTextModel); + const pickedMimeTypeRenderer = mimeTypes[pick]; + if (mimeTypes.length > 1) { + outputItemDiv.style.position = 'relative'; + const mimeTypePicker = DOM.$('.multi-mimetype-output'); + mimeTypePicker.classList.add(...ThemeIcon.asClassNameArray(mimetypeIcon)); + mimeTypePicker.tabIndex = 0; + mimeTypePicker.title = nls.localize('mimeTypePicker', "Choose a different output mimetype, available mimetypes: {0}", mimeTypes.map(mimeType => mimeType.mimeType).join(', ')); + outputItemDiv.appendChild(mimeTypePicker); + this.resizeListener.add(DOM.addStandardDisposableListener(mimeTypePicker, 'mousedown', async e => { + if (e.leftButton) { + e.preventDefault(); + e.stopPropagation(); + await this.pickActiveMimeTypeRenderer(this._notebookTextModel, this.output as IDisplayOutputViewModel); + } + })); + + this.resizeListener.add((DOM.addDisposableListener(mimeTypePicker, DOM.EventType.KEY_DOWN, async e => { + const event = new StandardKeyboardEvent(e); + if ((event.equals(KeyCode.Enter) || event.equals(KeyCode.Space))) { + e.preventDefault(); + e.stopPropagation(); + await this.pickActiveMimeTypeRenderer(this._notebookTextModel, this.output as IDisplayOutputViewModel); + } + }))); + } + + const innerContainer = DOM.$('.output-inner-container'); + DOM.append(outputItemDiv, innerContainer); + + + if (pickedMimeTypeRenderer.rendererId !== BUILTIN_RENDERER_ID) { + const renderer = this._notebookService.getRendererInfo(pickedMimeTypeRenderer.rendererId); + result = renderer + ? { type: RenderOutputType.Extension, renderer, source: this.output, mimeType: pickedMimeTypeRenderer.mimeType } + : this._notebookEditor.getOutputRenderer().render(this.output, innerContainer, pickedMimeTypeRenderer.mimeType, this._notebookTextModel.uri,); + } else { + result = this._notebookEditor.getOutputRenderer().render(this.output, innerContainer, pickedMimeTypeRenderer.mimeType, this._notebookTextModel.uri); + } + + this.output.pickedMimeType = pick; + } else { + // for text and error, there is no mimetype + const innerContainer = DOM.$('.output-inner-container'); + DOM.append(outputItemDiv, innerContainer); + + result = this._notebookEditor.getOutputRenderer().render(this.output, innerContainer, undefined, this._notebookTextModel.uri); + } + + this.domNode = outputItemDiv; + this.renderResult = result; + + if (!result) { + // this.viewCell.updateOutputHeight(index, 0); + return; + } + + if (beforeElement) { + this._outputContainer.insertBefore(outputItemDiv, beforeElement); + } else { + this._outputContainer.appendChild(outputItemDiv); + } + + if (result.type !== RenderOutputType.None) { + // this.viewCell.selfSizeMonitoring = true; + this._notebookEditor.createInset( + this._diffElementViewModel, + this._nestedCell, + result, + () => this.getOutputOffsetInCell(index), + this._diffElementViewModel instanceof SideBySideDiffElementViewModel + ? this._diffSide + : this._diffElementViewModel.type === 'insert' ? DiffSide.Modified : DiffSide.Original + ); + } else { + outputItemDiv.classList.add('foreground', 'output-element'); + outputItemDiv.style.position = 'absolute'; + } + + if (outputHasDynamicHeight(result)) { + // this.viewCell.selfSizeMonitoring = true; + const clientHeight = outputItemDiv.clientHeight; + // TODO, set an inital dimension to avoid force reflow + // const dimension = { + // width: this.cellViewModel., + // height: clientHeight + // }; + + const elementSizeObserver = getResizesObserver(outputItemDiv, undefined, () => { + if (this._outputContainer && document.body.contains(this._outputContainer)) { + const height = Math.ceil(elementSizeObserver.getHeight()); + + if (clientHeight === height) { + return; + } + + const currIndex = this.getCellOutputCurrentIndex(); + if (currIndex < 0) { + return; + } + + this.updateHeight(currIndex, height); + } + }); + elementSizeObserver.startObserving(); + this.resizeListener.add(elementSizeObserver); + this.updateHeight(index, clientHeight); + } else if (result.type === RenderOutputType.None) { // no-op if it's a webview + const clientHeight = Math.ceil(outputItemDiv.clientHeight); + this.updateHeight(index, clientHeight); + + const top = this.getOutputOffsetInContainer(index); + outputItemDiv.style.top = `${top}px`; + } + } + + private async pickActiveMimeTypeRenderer(notebookTextModel: NotebookTextModel, viewModel: IDisplayOutputViewModel) { + const [mimeTypes, currIndex] = viewModel.resolveMimeTypes(notebookTextModel); + + const items = mimeTypes.filter(mimeType => mimeType.isTrusted).map((mimeType, index): IMimeTypeRenderer => ({ + label: mimeType.mimeType, + id: mimeType.mimeType, + index: index, + picked: index === currIndex, + detail: this.generateRendererInfo(mimeType.rendererId), + description: index === currIndex ? nls.localize('curruentActiveMimeType', "Currently Active") : undefined + })); + + const picker = this._quickInputService.createQuickPick(); + picker.items = items; + picker.activeItems = items.filter(item => !!item.picked); + picker.placeholder = items.length !== mimeTypes.length + ? nls.localize('promptChooseMimeTypeInSecure.placeHolder', "Select mimetype to render for current output. Rich mimetypes are available only when the notebook is trusted") + : nls.localize('promptChooseMimeType.placeHolder', "Select mimetype to render for current output"); + + const pick = await new Promise(resolve => { + picker.onDidAccept(() => { + resolve(picker.selectedItems.length === 1 ? (picker.selectedItems[0] as IMimeTypeRenderer).index : undefined); + picker.dispose(); + }); + picker.show(); + }); + + if (pick === undefined) { + return; + } + + if (pick !== currIndex) { + // user chooses another mimetype + const index = this._nestedCell.outputsViewModels.indexOf(viewModel); + const nextElement = this.domNode.nextElementSibling; + this.resizeListener.clear(); + const element = this.domNode; + if (element) { + element.parentElement?.removeChild(element); + this._notebookEditor.removeInset( + this._diffElementViewModel, + this._nestedCell, + viewModel, + this._diffSide + ); + } + + viewModel.pickedMimeType = pick; + this.render(index, nextElement as HTMLElement); + } + } + + private generateRendererInfo(renderId: string | undefined): string { + if (renderId === undefined || renderId === BUILTIN_RENDERER_ID) { + return nls.localize('builtinRenderInfo', "built-in"); + } + + const renderInfo = this._notebookService.getRendererInfo(renderId); + + if (renderInfo) { + const displayName = renderInfo.displayName !== '' ? renderInfo.displayName : renderInfo.id; + return `${displayName} (${renderInfo.extensionId.value})`; + } + + return nls.localize('builtinRenderInfo', "built-in"); + } + + getCellOutputCurrentIndex() { + return this._diffElementViewModel.getNestedCellViewModel(this._diffSide).outputs.indexOf(this.output.model); + } + + updateHeight(index: number, height: number) { + this._diffElementViewModel.updateOutputHeight(this._diffSide, index, height); + } + + getOutputOffsetInContainer(index: number) { + return this._diffElementViewModel.getOutputOffsetInContainer(this._diffSide, index); + } + + getOutputOffsetInCell(index: number) { + return this._diffElementViewModel.getOutputOffsetInCell(this._diffSide, index); + } +} + +export class OutputContainer extends Disposable { + private _outputEntries = new Map(); + constructor( + private _editor: INotebookTextDiffEditor, + private _notebookTextModel: NotebookTextModel, + private _diffElementViewModel: DiffElementViewModelBase, + private _nestedCellViewModel: DiffNestedCellViewModel, + private _diffSide: DiffSide, + private _outputContainer: HTMLElement, + @INotebookService private _notebookService: INotebookService, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @IOpenerService readonly _openerService: IOpenerService, + @ITextFileService readonly _textFileService: ITextFileService, + + ) { + super(); + this._register(this._diffElementViewModel.onDidLayoutChange(() => { + this._outputEntries.forEach((value, key) => { + const index = _nestedCellViewModel.outputs.indexOf(key.model); + if (index >= 0) { + const top = this._diffElementViewModel.getOutputOffsetInContainer(this._diffSide, index); + value.domNode.style.top = `${top}px`; + } + }); + })); + + this._register(this._nestedCellViewModel.textModel.onDidChangeOutputs(splices => { + this._updateOutputs(splices); + })); + } + + private _updateOutputs(splices: NotebookCellOutputsSplice[]) { + if (!splices.length) { + return; + } + + const removedKeys: ICellOutputViewModel[] = []; + + this._outputEntries.forEach((value, key) => { + if (this._nestedCellViewModel.outputsViewModels.indexOf(key) < 0) { + // already removed + removedKeys.push(key); + // remove element from DOM + this._outputContainer.removeChild(value.domNode); + if (key.isDisplayOutput()) { + this._editor.removeInset(this._diffElementViewModel, this._nestedCellViewModel, key, this._diffSide); + } + } + }); + + removedKeys.forEach(key => { + this._outputEntries.get(key)?.dispose(); + this._outputEntries.delete(key); + }); + + let prevElement: HTMLElement | undefined = undefined; + const outputsToRender = this._nestedCellViewModel.outputsViewModels; + + outputsToRender.reverse().forEach(output => { + if (this._outputEntries.has(output)) { + // already exist + prevElement = this._outputEntries.get(output)!.domNode; + return; + } + + // newly added element + const currIndex = this._nestedCellViewModel.outputsViewModels.indexOf(output); + this._renderOutput(output, currIndex, prevElement); + prevElement = this._outputEntries.get(output)?.domNode; + }); + } + render() { + // TODO, outputs to render (should have a limit) + for (let index = 0; index < this._nestedCellViewModel.outputsViewModels.length; index++) { + const currOutput = this._nestedCellViewModel.outputsViewModels[index]; + + // always add to the end + this._renderOutput(currOutput, index, undefined); + } + } + + showOutputs() { + for (let index = 0; index < this._nestedCellViewModel.outputsViewModels.length; index++) { + const currOutput = this._nestedCellViewModel.outputsViewModels[index]; + + if (currOutput.isDisplayOutput()) { + // always add to the end + this._editor.showInset(this._diffElementViewModel, currOutput.cellViewModel, currOutput, this._diffSide); + } + } + } + + hideOutputs() { + this._outputEntries.forEach((outputElement, cellOutputViewModel) => { + if (cellOutputViewModel.isDisplayOutput()) { + this._editor.hideInset(this._diffElementViewModel, this._nestedCellViewModel, cellOutputViewModel); + } + }); + } + + private _renderOutput(currOutput: ICellOutputViewModel, index: number, beforeElement?: HTMLElement) { + if (!this._outputEntries.has(currOutput)) { + this._outputEntries.set(currOutput, new OutputElement(this._editor, this._notebookTextModel, this._notebookService, this._quickInputService, this._diffElementViewModel, this._diffSide, this._nestedCellViewModel, this._outputContainer, currOutput)); + } + + const renderElement = this._outputEntries.get(currOutput)!; + renderElement.render(index, beforeElement); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts new file mode 100644 index 000000000..4d10ed220 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -0,0 +1,498 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { CellDiffViewModelLayoutChangeEvent, DiffSide, DIFF_CELL_MARGIN, IDiffElementLayoutInfo } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { IGenericCellViewModel, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { hash } from 'vs/base/common/hash'; +import { format } from 'vs/base/common/jsonFormatter'; +import { applyEdits } from 'vs/base/common/jsonEdit'; +import { NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; +import { URI } from 'vs/base/common/uri'; +import { NotebookDiffEditorEventDispatcher, NotebookDiffViewEventType } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; +import * as editorCommon from 'vs/editor/common/editorCommon'; + +export enum PropertyFoldingState { + Expanded, + Collapsed +} + +export const OUTPUT_EDITOR_HEIGHT_MAGIC = 1440; + +type ILayoutInfoDelta0 = { [K in keyof IDiffElementLayoutInfo]?: number; }; +interface ILayoutInfoDelta extends ILayoutInfoDelta0 { + rawOutputHeight?: number; + recomputeOutput?: boolean; +} + +export abstract class DiffElementViewModelBase extends Disposable { + public metadataFoldingState: PropertyFoldingState; + public outputFoldingState: PropertyFoldingState; + protected _layoutInfoEmitter = new Emitter(); + onDidLayoutChange = this._layoutInfoEmitter.event; + protected _stateChangeEmitter = new Emitter<{ renderOutput: boolean; }>(); + onDidStateChange = this._stateChangeEmitter.event; + protected _layoutInfo!: IDiffElementLayoutInfo; + + set rawOutputHeight(height: number) { + this._layout({ rawOutputHeight: Math.min(OUTPUT_EDITOR_HEIGHT_MAGIC, height) }); + } + + get rawOutputHeight() { + throw new Error('Use Cell.layoutInfo.rawOutputHeight'); + } + + set outputStatusHeight(height: number) { + this._layout({ outputStatusHeight: height }); + } + + get outputStatusHeight() { + throw new Error('Use Cell.layoutInfo.outputStatusHeight'); + } + + set editorHeight(height: number) { + this._layout({ editorHeight: height }); + } + + get editorHeight() { + throw new Error('Use Cell.layoutInfo.editorHeight'); + } + + set editorMargin(margin: number) { + this._layout({ editorMargin: margin }); + } + + get editorMargin() { + throw new Error('Use Cell.layoutInfo.editorMargin'); + } + + set metadataHeight(height: number) { + this._layout({ metadataHeight: height }); + } + + get metadataHeight() { + throw new Error('Use Cell.layoutInfo.metadataHeight'); + } + + private _renderOutput = true; + + set renderOutput(value: boolean) { + this._renderOutput = value; + this._layout({ recomputeOutput: true }); + this._stateChangeEmitter.fire({ renderOutput: this._renderOutput }); + } + + get renderOutput() { + return this._renderOutput; + } + + get layoutInfo(): IDiffElementLayoutInfo { + return this._layoutInfo; + } + + private _sourceEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; + private _outputEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; + private _metadataEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; + + constructor( + readonly mainDocumentTextModel: NotebookTextModel, + readonly original: DiffNestedCellViewModel | undefined, + readonly modified: DiffNestedCellViewModel | undefined, + readonly type: 'unchanged' | 'insert' | 'delete' | 'modified', + readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher + ) { + super(); + this._layoutInfo = { + width: 0, + editorHeight: 0, + editorMargin: 0, + metadataHeight: 0, + metadataStatusHeight: 25, + rawOutputHeight: 0, + outputTotalHeight: 0, + outputStatusHeight: 25, + bodyMargin: 32, + totalHeight: 82 + }; + + this.metadataFoldingState = PropertyFoldingState.Collapsed; + this.outputFoldingState = PropertyFoldingState.Collapsed; + + this._register(this.editorEventDispatcher.onDidChangeLayout(e => { + this._layoutInfoEmitter.fire({ outerWidth: true }); + })); + } + + layoutChange() { + this._layout({ recomputeOutput: true }); + } + + protected _layout(delta: ILayoutInfoDelta) { + const width = delta.width !== undefined ? delta.width : this._layoutInfo.width; + const editorHeight = delta.editorHeight !== undefined ? delta.editorHeight : this._layoutInfo.editorHeight; + const editorMargin = delta.editorMargin !== undefined ? delta.editorMargin : this._layoutInfo.editorMargin; + const metadataHeight = delta.metadataHeight !== undefined ? delta.metadataHeight : this._layoutInfo.metadataHeight; + const metadataStatusHeight = delta.metadataStatusHeight !== undefined ? delta.metadataStatusHeight : this._layoutInfo.metadataStatusHeight; + const rawOutputHeight = delta.rawOutputHeight !== undefined ? delta.rawOutputHeight : this._layoutInfo.rawOutputHeight; + const outputStatusHeight = delta.outputStatusHeight !== undefined ? delta.outputStatusHeight : this._layoutInfo.outputStatusHeight; + const bodyMargin = delta.bodyMargin !== undefined ? delta.bodyMargin : this._layoutInfo.bodyMargin; + const outputHeight = (delta.recomputeOutput || delta.rawOutputHeight !== undefined) ? this._getOutputTotalHeight(rawOutputHeight) : this._layoutInfo.outputTotalHeight; + + const totalHeight = editorHeight + + editorMargin + + metadataHeight + + metadataStatusHeight + + outputHeight + + outputStatusHeight + + bodyMargin; + + const newLayout: IDiffElementLayoutInfo = { + width: width, + editorHeight: editorHeight, + editorMargin: editorMargin, + metadataHeight: metadataHeight, + metadataStatusHeight: metadataStatusHeight, + outputTotalHeight: outputHeight, + outputStatusHeight: outputStatusHeight, + bodyMargin: bodyMargin, + rawOutputHeight: rawOutputHeight, + totalHeight: totalHeight + }; + + const changeEvent: CellDiffViewModelLayoutChangeEvent = {}; + + if (newLayout.width !== this._layoutInfo.width) { + changeEvent.width = true; + } + + if (newLayout.editorHeight !== this._layoutInfo.editorHeight) { + changeEvent.editorHeight = true; + } + + if (newLayout.editorMargin !== this._layoutInfo.editorMargin) { + changeEvent.editorMargin = true; + } + + if (newLayout.metadataHeight !== this._layoutInfo.metadataHeight) { + changeEvent.metadataHeight = true; + } + + if (newLayout.metadataStatusHeight !== this._layoutInfo.metadataStatusHeight) { + changeEvent.metadataStatusHeight = true; + } + + if (newLayout.outputTotalHeight !== this._layoutInfo.outputTotalHeight) { + changeEvent.outputTotalHeight = true; + } + + if (newLayout.outputStatusHeight !== this._layoutInfo.outputStatusHeight) { + changeEvent.outputStatusHeight = true; + } + + if (newLayout.bodyMargin !== this._layoutInfo.bodyMargin) { + changeEvent.bodyMargin = true; + } + + if (newLayout.totalHeight !== this._layoutInfo.totalHeight) { + changeEvent.totalHeight = true; + } + + this._layoutInfo = newLayout; + this._fireLayoutChangeEvent(changeEvent); + } + + private _getOutputTotalHeight(rawOutputHeight: number) { + if (this.outputFoldingState === PropertyFoldingState.Collapsed) { + return 0; + } + + if (this.renderOutput) { + if (this.isOutputEmpty()) { + // single line; + return 24; + } + return this.getRichOutputTotalHeight(); + } else { + return rawOutputHeight; + } + } + + private _fireLayoutChangeEvent(state: CellDiffViewModelLayoutChangeEvent) { + this._layoutInfoEmitter.fire(state); + this.editorEventDispatcher.emit([{ type: NotebookDiffViewEventType.CellLayoutChanged, source: this._layoutInfo }]); + } + + abstract checkIfOutputsModified(): boolean; + abstract checkMetadataIfModified(): boolean; + abstract isOutputEmpty(): boolean; + abstract getRichOutputTotalHeight(): number; + abstract getCellByUri(cellUri: URI): IGenericCellViewModel; + abstract getOutputOffsetInCell(diffSide: DiffSide, index: number): number; + abstract getOutputOffsetInContainer(diffSide: DiffSide, index: number): number; + abstract updateOutputHeight(diffSide: DiffSide, index: number, height: number): void; + abstract getNestedCellViewModel(diffSide: DiffSide): DiffNestedCellViewModel; + + getComputedCellContainerWidth(layoutInfo: NotebookLayoutInfo, diffEditor: boolean, fullWidth: boolean) { + if (fullWidth) { + return layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0) - 2; + } + + return (layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)) / 2 - 18 - 2; + } + + getOutputEditorViewState(): editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null { + return this._outputEditorViewState; + } + + saveOutputEditorViewState(viewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null) { + this._outputEditorViewState = viewState; + } + + getMetadataEditorViewState(): editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null { + return this._metadataEditorViewState; + } + + saveMetadataEditorViewState(viewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null) { + this._metadataEditorViewState = viewState; + } + + getSourceEditorViewState(): editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null { + return this._sourceEditorViewState; + } + + saveSpirceEditorViewState(viewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null) { + this._sourceEditorViewState = viewState; + } +} + +export class SideBySideDiffElementViewModel extends DiffElementViewModelBase { + get originalDocument() { + return this.otherDocumentTextModel; + } + + get modifiedDocument() { + return this.mainDocumentTextModel; + } + + constructor( + readonly mainDocumentTextModel: NotebookTextModel, + readonly otherDocumentTextModel: NotebookTextModel, + readonly original: DiffNestedCellViewModel, + readonly modified: DiffNestedCellViewModel, + readonly type: 'unchanged' | 'modified', + readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher + ) { + super( + mainDocumentTextModel, + original, + modified, + type, + editorEventDispatcher); + + this.metadataFoldingState = PropertyFoldingState.Collapsed; + this.outputFoldingState = PropertyFoldingState.Collapsed; + + if (this.checkMetadataIfModified()) { + this.metadataFoldingState = PropertyFoldingState.Expanded; + } + + if (this.checkIfOutputsModified()) { + this.outputFoldingState = PropertyFoldingState.Expanded; + } + + this._register(this.original.onDidChangeOutputLayout(() => { + this._layout({ recomputeOutput: true }); + })); + + this._register(this.modified.onDidChangeOutputLayout(() => { + this._layout({ recomputeOutput: true }); + })); + } + + checkIfOutputsModified() { + return !this.mainDocumentTextModel.transientOptions.transientOutputs && hash(this.original?.outputs ?? []) !== hash(this.modified?.outputs ?? []); + } + + checkMetadataIfModified(): boolean { + return hash(getFormatedMetadataJSON(this.mainDocumentTextModel, this.original?.metadata || {}, this.original?.language)) !== hash(getFormatedMetadataJSON(this.mainDocumentTextModel, this.modified?.metadata ?? {}, this.modified?.language)); + } + + updateOutputHeight(diffSide: DiffSide, index: number, height: number) { + if (diffSide === DiffSide.Original) { + this.original.updateOutputHeight(index, height); + } else { + this.modified.updateOutputHeight(index, height); + } + } + + getOutputOffsetInContainer(diffSide: DiffSide, index: number) { + if (diffSide === DiffSide.Original) { + return this.original.getOutputOffset(index); + } else { + return this.modified.getOutputOffset(index); + } + } + + getOutputOffsetInCell(diffSide: DiffSide, index: number) { + const offsetInOutputsContainer = this.getOutputOffsetInContainer(diffSide, index); + + return this._layoutInfo.editorHeight + + this._layoutInfo.editorMargin + + this._layoutInfo.metadataHeight + + this._layoutInfo.metadataStatusHeight + + this._layoutInfo.outputStatusHeight + + this._layoutInfo.bodyMargin / 2 + + offsetInOutputsContainer; + } + + isOutputEmpty() { + if (this.mainDocumentTextModel.transientOptions.transientOutputs) { + return true; + } + + if (this.checkIfOutputsModified()) { + return false; + } + + // outputs are not changed + + return (this.original?.outputs || []).length === 0; + } + + getRichOutputTotalHeight() { + return Math.max(this.original.getOutputTotalHeight(), this.modified.getOutputTotalHeight()); + } + + getNestedCellViewModel(diffSide: DiffSide): DiffNestedCellViewModel { + throw new Error('Method not implemented.'); + } + + getCellByUri(cellUri: URI): IGenericCellViewModel { + if (cellUri.toString() === this.original.uri.toString()) { + return this.original; + } else { + return this.modified; + } + } +} + +export class SingleSideDiffElementViewModel extends DiffElementViewModelBase { + get cellViewModel() { + return this.type === 'insert' ? this.modified! : this.original!; + } + + get originalDocument() { + if (this.type === 'insert') { + return this.otherDocumentTextModel; + } else { + return this.mainDocumentTextModel; + } + } + + get modifiedDocument() { + if (this.type === 'insert') { + return this.mainDocumentTextModel; + } else { + return this.otherDocumentTextModel; + } + } + + constructor( + readonly mainDocumentTextModel: NotebookTextModel, + readonly otherDocumentTextModel: NotebookTextModel, + readonly original: DiffNestedCellViewModel | undefined, + readonly modified: DiffNestedCellViewModel | undefined, + readonly type: 'insert' | 'delete', + readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher + ) { + super(mainDocumentTextModel, original, modified, type, editorEventDispatcher); + this._register(this.cellViewModel!.onDidChangeOutputLayout(() => { + this._layout({ recomputeOutput: true }); + })); + } + + getNestedCellViewModel(diffSide: DiffSide): DiffNestedCellViewModel { + return this.type === 'insert' ? this.modified! : this.original!; + } + + + checkIfOutputsModified(): boolean { + return false; + } + + checkMetadataIfModified(): boolean { + return false; + } + + updateOutputHeight(diffSide: DiffSide, index: number, height: number) { + this.cellViewModel?.updateOutputHeight(index, height); + } + + getOutputOffsetInContainer(diffSide: DiffSide, index: number) { + return this.cellViewModel!.getOutputOffset(index); + } + + getOutputOffsetInCell(diffSide: DiffSide, index: number) { + const offsetInOutputsContainer = this.cellViewModel!.getOutputOffset(index); + + return this._layoutInfo.editorHeight + + this._layoutInfo.editorMargin + + this._layoutInfo.metadataHeight + + this._layoutInfo.metadataStatusHeight + + this._layoutInfo.outputStatusHeight + + this._layoutInfo.bodyMargin / 2 + + offsetInOutputsContainer; + } + + isOutputEmpty() { + if (this.mainDocumentTextModel.transientOptions.transientOutputs) { + return true; + } + + // outputs are not changed + + return (this.original?.outputs || this.modified?.outputs || []).length === 0; + } + + getRichOutputTotalHeight() { + return this.cellViewModel?.getOutputTotalHeight() ?? 0; + } + + getCellByUri(cellUri: URI): IGenericCellViewModel { + return this.cellViewModel!; + } +} + +export function getFormatedMetadataJSON(documentTextModel: NotebookTextModel, metadata: NotebookCellMetadata, language?: string) { + let filteredMetadata: { [key: string]: any } = {}; + + if (documentTextModel) { + const transientMetadata = documentTextModel.transientOptions.transientMetadata; + + const keys = new Set([...Object.keys(metadata)]); + for (let key of keys) { + if (!(transientMetadata[key as keyof NotebookCellMetadata]) + ) { + filteredMetadata[key] = metadata[key as keyof NotebookCellMetadata]; + } + } + } else { + filteredMetadata = metadata; + } + + const content = JSON.stringify({ + language, + ...filteredMetadata + }); + + const edits = format(content, undefined, {}); + const metadataSource = applyEdits(content, edits); + + return metadataSource; +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts new file mode 100644 index 000000000..bd9f8d2ad --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { generateUuid } from 'vs/base/common/uuid'; +import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; +import { IDiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { CellViewModelStateChangeEvent, ICellOutputViewModel, IGenericCellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellOutputViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/cellOutputViewModel'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; + +export class DiffNestedCellViewModel extends Disposable implements IDiffNestedCellViewModel, IGenericCellViewModel { + private _id: string; + get id() { + return this._id; + } + + get outputs() { + return this.textModel.outputs; + } + + get language() { + return this.textModel.language; + } + + get metadata() { + return this.textModel.metadata; + } + + get uri() { + return this.textModel.uri; + } + + get handle() { + return this.textModel.handle; + } + + protected readonly _onDidChangeState: Emitter = this._register(new Emitter()); + + private _hoveringOutput: boolean = false; + public get outputIsHovered(): boolean { + return this._hoveringOutput; + } + + public set outputIsHovered(v: boolean) { + this._hoveringOutput = v; + this._onDidChangeState.fire({ outputIsHoveredChanged: true }); + } + private _outputViewModels: ICellOutputViewModel[]; + + get outputsViewModels() { + return this._outputViewModels; + } + + protected _outputCollection: number[] = []; + protected _outputsTop: PrefixSumComputer | null = null; + protected readonly _onDidChangeOutputLayout = new Emitter(); + readonly onDidChangeOutputLayout = this._onDidChangeOutputLayout.event; + + + constructor( + readonly textModel: NotebookCellTextModel, + @INotebookService private _notebookService: INotebookService + ) { + super(); + this._id = generateUuid(); + + this._outputViewModels = this.textModel.outputs.map(output => new CellOutputViewModel(this, output, this._notebookService)); + this._register(this.textModel.onDidChangeOutputs((splices) => { + splices.reverse().forEach(splice => { + this._outputCollection.splice(splice[0], splice[1], ...splice[2].map(() => 0)); + this._outputViewModels.splice(splice[0], splice[1], ...splice[2].map(output => new CellOutputViewModel(this, output, this._notebookService))); + }); + + this._outputsTop = null; + this._onDidChangeOutputLayout.fire(); + })); + this._outputCollection = new Array(this.textModel.outputs.length); + } + + private _ensureOutputsTop() { + if (!this._outputsTop) { + const values = new Uint32Array(this._outputCollection.length); + for (let i = 0; i < this._outputCollection.length; i++) { + values[i] = this._outputCollection[i]; + } + + this._outputsTop = new PrefixSumComputer(values); + } + } + + getOutputOffset(index: number): number { + this._ensureOutputsTop(); + + if (index >= this._outputCollection.length) { + throw new Error('Output index out of range!'); + } + + return this._outputsTop!.getAccumulatedValue(index - 1); + } + + updateOutputHeight(index: number, height: number): void { + if (index >= this._outputCollection.length) { + throw new Error('Output index out of range!'); + } + + this._ensureOutputsTop(); + this._outputCollection[index] = height; + if (this._outputsTop!.changeValue(index, height)) { + this._onDidChangeOutputLayout.fire(); + } + } + + getOutputTotalHeight() { + this._ensureOutputsTop(); + + return this._outputsTop?.getTotalValue() ?? 0; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/eventDispatcher.ts b/src/vs/workbench/contrib/notebook/browser/diff/eventDispatcher.ts new file mode 100644 index 000000000..2e4cb22ee --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/eventDispatcher.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { IDiffElementLayoutInfo } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { NotebookLayoutChangeEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; + +export enum NotebookDiffViewEventType { + LayoutChanged = 1, + CellLayoutChanged = 2 + // MetadataChanged = 2, + // CellStateChanged = 3 +} + +export class NotebookDiffLayoutChangedEvent { + public readonly type = NotebookDiffViewEventType.LayoutChanged; + + constructor(readonly source: NotebookLayoutChangeEvent, readonly value: NotebookLayoutInfo) { + + } +} + +export class NotebookCellLayoutChangedEvent { + public readonly type = NotebookDiffViewEventType.CellLayoutChanged; + + constructor(readonly source: IDiffElementLayoutInfo) { + + } +} + +export type NotebookDiffViewEvent = NotebookDiffLayoutChangedEvent | NotebookCellLayoutChangedEvent; + +export class NotebookDiffEditorEventDispatcher { + protected readonly _onDidChangeLayout = new Emitter(); + readonly onDidChangeLayout = this._onDidChangeLayout.event; + protected readonly _onDidChangeCellLayout = new Emitter(); + readonly onDidChangeCellLayout = this._onDidChangeCellLayout.event; + + constructor() { + } + + emit(events: NotebookDiffViewEvent[]) { + for (let i = 0, len = events.length; i < len; i++) { + const e = events[i]; + + switch (e.type) { + case NotebookDiffViewEventType.LayoutChanged: + this._onDidChangeLayout.fire(e); + break; + case NotebookDiffViewEventType.CellLayoutChanged: + this._onDidChangeCellLayout.fire(e); + break; + } + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css index 950cbce2f..407465444 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css @@ -21,6 +21,31 @@ flex-direction: row; } +.notebook-text-diff-editor .webview-cover { + user-select: initial; + -webkit-user-select: initial; +} + +.notebook-text-diff-editor .cell-body .border-container { + position: absolute; + width: calc(100% - 32px); +} + +.notebook-text-diff-editor .cell-body .border-container .top-border, +.notebook-text-diff-editor .cell-body .border-container .bottom-border { + position: absolute; + width: 100%; +} + +.notebook-text-diff-editor .cell-body .border-container .left-border, +.notebook-text-diff-editor .cell-body .border-container .right-border { + position: absolute; +} + +.notebook-text-diff-editor .cell-body .border-container .right-border { + left: 100%; +} + .notebook-text-diff-editor .cell-body.right { flex-direction: row-reverse; } @@ -32,18 +57,20 @@ .notebook-text-diff-editor .cell-body .cell-diff-editor-container { width: 100%; - overflow: hidden; + /* why we overflow hidden at the beginning?*/ + /* overflow: hidden; */ } .notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container.diff, .notebook-text-diff-editor .cell-body .cell-diff-editor-container .output-editor-container.diff, .notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff { /** 100% + diffOverviewWidth */ - width: calc(100% + 30px); + width: calc(100%); } .notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container .monaco-diff-editor .diffOverview, -.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff .monaco-diff-editor .diffOverview { +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff .monaco-diff-editor .diffOverview, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .output-editor-container.diff .monaco-diff-editor .diffOverview { display: none; } @@ -106,10 +133,15 @@ overflow: hidden; } +.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { + overflow: visible !important; +} + .monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row, .monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover, .monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: none !important; + background-color: transparent !important; } .notebook-text-diff-editor .cell-diff-editor-container .editor-input-toolbar-container { @@ -118,3 +150,120 @@ top: 16px; margin: 4px 2px; } + +.monaco-workbench .notebook-text-diff-editor .cell-body { + height: 0; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body .output-view-container { + user-select: text; + -webkit-user-select: text; + -ms-user-select: text; + white-space: initial; + cursor: auto; + position: relative; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body .output-view-container .output-plaintext { + white-space: pre; + overflow-x: hidden; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.left .output-view-container .output-inner-container, +.monaco-workbench .notebook-text-diff-editor .cell-body.right .output-view-container .output-inner-container { + width: 100%; + padding: 0px 8px; + box-sizing: border-box; + overflow-x: hidden; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.left .output-view-container .output-inner-container { + padding: 0px 8px 0px 32px; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.right .output-view-container .output-inner-container { + padding: 0px 8px 0px 32px; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-view-container .output-inner-container { + width: 100%; + padding: 4px 8px 4px 32px; + box-sizing: border-box; + overflow-x: hidden; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-left { + top: 0; + position: absolute; + left: 0; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-right { + position: absolute; + top: 0; + left: 50%; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-left, +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-right { + width: 50%; + display: inline-block; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-left div.foreground, +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-right div.foreground { + width: 100%; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container > div.foreground { + width: 100%; + min-height: 24px; + box-sizing: border-box; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .error_message { + color: red; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .error > div { + white-space: normal; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .error pre.traceback { + box-sizing: border-box; + padding: 8px 0; + margin: 0px; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .error .traceback > span { + display: block; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .display img { + max-width: 100%; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .multi-mimetype-output { + position: absolute; + top: 4px; + left: 8px; + width: 16px; + height: 16px; + cursor: pointer; + padding: 2px 4px 4px 2px; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .output-empty-view span { + opacity: 0.7; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .output-empty-view { + font-style: italic; + height: 24px; + margin: auto; + padding-left: 12px; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container pre { + margin: 4px 0; +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts index cb8709d16..0911630f6 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts @@ -8,10 +8,11 @@ import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ActiveEditorContext, viewColumnToEditorGroup } from 'vs/workbench/common/editor'; -import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { DiffElementViewModelBase } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor'; import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput'; -import { openAsTextIcon, revertIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { openAsTextIcon, renderOutputIcon, revertIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -60,12 +61,14 @@ registerAction2(class extends Action2 { icon: revertIcon, f1: false, menu: { - id: MenuId.NotebookDiffCellMetadataTitle - } + id: MenuId.NotebookDiffCellMetadataTitle, + when: NOTEBOOK_DIFF_CELL_PROPERTY + }, + precondition: NOTEBOOK_DIFF_CELL_PROPERTY } ); } - run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) { + run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { if (!context) { return; } @@ -77,7 +80,55 @@ registerAction2(class extends Action2 { return; } - modified.metadata = original.metadata; + modified.textModel.metadata = original.metadata; + } +}); + +// registerAction2(class extends Action2 { +// constructor() { +// super( +// { +// id: 'notebook.diff.cell.switchOutputRenderingStyle', +// title: localize('notebook.diff.cell.switchOutputRenderingStyle', "Switch Outputs Rendering"), +// icon: renderOutputIcon, +// f1: false, +// menu: { +// id: MenuId.NotebookDiffCellOutputsTitle +// } +// } +// ); +// } +// run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { +// if (!context) { +// return; +// } + +// context.cell.renderOutput = true; +// } +// }); + + +registerAction2(class extends Action2 { + constructor() { + super( + { + id: 'notebook.diff.cell.switchOutputRenderingStyleToText', + title: localize('notebook.diff.cell.switchOutputRenderingStyleToText', "Switch Output Rendering"), + icon: renderOutputIcon, + f1: false, + menu: { + id: MenuId.NotebookDiffCellOutputsTitle, + when: NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED + } + } + ); + } + run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { + if (!context) { + return; + } + + context.cell.renderOutput = !context.cell.renderOutput; } }); @@ -90,12 +141,14 @@ registerAction2(class extends Action2 { icon: revertIcon, f1: false, menu: { - id: MenuId.NotebookDiffCellOutputsTitle - } + id: MenuId.NotebookDiffCellOutputsTitle, + when: NOTEBOOK_DIFF_CELL_PROPERTY + }, + precondition: NOTEBOOK_DIFF_CELL_PROPERTY } ); } - run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) { + run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { if (!context) { return; } @@ -107,10 +160,11 @@ registerAction2(class extends Action2 { return; } - modified.spliceNotebookCellOutputs([[0, modified.outputs.length, original.outputs]]); + modified.textModel.spliceNotebookCellOutputs([[0, modified.outputs.length, original.outputs]]); } }); + registerAction2(class extends Action2 { constructor() { super( @@ -120,12 +174,15 @@ registerAction2(class extends Action2 { icon: revertIcon, f1: false, menu: { - id: MenuId.NotebookDiffCellInputTitle - } + id: MenuId.NotebookDiffCellInputTitle, + when: NOTEBOOK_DIFF_CELL_PROPERTY + }, + precondition: NOTEBOOK_DIFF_CELL_PROPERTY + } ); } - run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) { + run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { if (!context) { return; } @@ -139,7 +196,7 @@ registerAction2(class extends Action2 { const bulkEditService = accessor.get(IBulkEditService); return bulkEditService.apply([ - new ResourceTextEdit(modified.uri, { range: modified.getFullModelRange(), text: original.getValue() }), + new ResourceTextEdit(modified.uri, { range: modified.textModel.getFullModelRange(), text: original.textModel.getValue() }), ], { quotableLabel: 'Split Notebook Cell' }); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts new file mode 100644 index 000000000..611b0f264 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICommonCellInfo, ICommonNotebookEditor, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DiffElementViewModelBase } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { Event } from 'vs/base/common/event'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export enum DiffSide { + Original = 0, + Modified = 1 +} + +export interface IDiffCellInfo extends ICommonCellInfo { + diffElement: DiffElementViewModelBase; +} + +export interface INotebookTextDiffEditor extends ICommonNotebookEditor { + readonly textModel?: NotebookTextModel; + onMouseUp: Event<{ readonly event: MouseEvent; readonly target: DiffElementViewModelBase; }>; + onDidDynamicOutputRendered: Event<{ cell: IGenericCellViewModel, output: IDisplayOutputViewModel }>; + getOverflowContainerDomNode(): HTMLElement; + getLayoutInfo(): NotebookLayoutInfo; + layoutNotebookCell(cell: DiffElementViewModelBase, height: number): void; + getOutputRenderer(): OutputRenderer; + createInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: IInsetRenderOutput, getOffset: () => number, diffSide: DiffSide): void; + showInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, displayOutput: IDisplayOutputViewModel, diffSide: DiffSide): void; + removeInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: IDisplayOutputViewModel, diffSide: DiffSide): void; + hideInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: IDisplayOutputViewModel): void; + /** + * Trigger the editor to scroll from scroll event programmatically + */ + triggerScroll(event: IMouseWheelEvent): void; + getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel; + focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; + focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; + updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean): void; + deltaCellOutputContainerClassNames(diffSide: DiffSide, cellId: string, added: string[], removed: string[]): void; +} + +export interface IDiffNestedCellViewModel { + +} + +export interface CellDiffCommonRenderTemplate { + readonly leftBorder: HTMLElement; + readonly rightBorder: HTMLElement; + readonly topBorder: HTMLElement; + readonly bottomBorder: HTMLElement; +} + +export interface CellDiffSingleSideRenderTemplate extends CellDiffCommonRenderTemplate { + readonly container: HTMLElement; + readonly body: HTMLElement; + readonly diffEditorContainer: HTMLElement; + readonly diagonalFill: HTMLElement; + readonly elementDisposables: DisposableStore; + readonly sourceEditor: CodeEditorWidget; + readonly metadataHeaderContainer: HTMLElement; + readonly metadataInfoContainer: HTMLElement; + readonly outputHeaderContainer: HTMLElement; + readonly outputInfoContainer: HTMLElement; + +} + + +export interface CellDiffSideBySideRenderTemplate extends CellDiffCommonRenderTemplate { + readonly container: HTMLElement; + readonly body: HTMLElement; + readonly diffEditorContainer: HTMLElement; + readonly elementDisposables: DisposableStore; + readonly sourceEditor: DiffEditorWidget; + readonly editorContainer: HTMLElement; + readonly inputToolbarContainer: HTMLElement; + readonly toolbar: ToolBar; + readonly metadataHeaderContainer: HTMLElement; + readonly metadataInfoContainer: HTMLElement; + readonly outputHeaderContainer: HTMLElement; + readonly outputInfoContainer: HTMLElement; +} + +export interface IDiffElementLayoutInfo { + totalHeight: number; + width: number; + editorHeight: number; + editorMargin: number; + metadataHeight: number; + metadataStatusHeight: number; + rawOutputHeight: number; + outputTotalHeight: number; + outputStatusHeight: number; + bodyMargin: number +} + +type IDiffElementSelfLayoutChangeEvent = { [K in keyof IDiffElementLayoutInfo]?: boolean }; + +export interface CellDiffViewModelLayoutChangeEvent extends IDiffElementSelfLayoutChangeEvent { + font?: BareFontInfo; + outerWidth?: boolean; + metadataEditor?: boolean; + outputEditor?: boolean; + outputView?: boolean; +} + +export const DIFF_CELL_MARGIN = 16; +export const NOTEBOOK_DIFF_CELL_PROPERTY = new RawContextKey('notebookDiffCellPropertyChanged', false); +export const NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED = new RawContextKey('notebookDiffCellPropertyExpanded', false); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 13f6973c0..19d4f07ef 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -13,32 +13,38 @@ import { notebookCellBorder, NotebookEditorWidget } from 'vs/workbench/contrib/n import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { NotebookDiffEditorInput } from '../notebookDiffEditorInput'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { WorkbenchList } from 'vs/platform/list/browser/listService'; -import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { DiffElementViewModelBase, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CellDiffRenderer, NotebookCellTextDiffListDelegate, NotebookTextDiffList } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList'; -import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { CellDiffSideBySideRenderer, CellDiffSingleSideRenderer, NotebookCellTextDiffListDelegate, NotebookTextDiffList } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { diffDiagonalFill, diffInserted, diffRemoved, editorBackground, focusBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { getZoomLevel } from 'vs/base/browser/browser'; -import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; +import { IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DiffSide, DIFF_CELL_MARGIN, IDiffCellInfo, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { Emitter } from 'vs/base/common/event'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { NotebookDiffEditorEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { INotebookDiffEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, INotebookDiffEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { FileService } from 'vs/platform/files/common/fileService'; import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { IDiffChange } from 'vs/base/common/diff/diff'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; +import { SequencerByKey } from 'vs/base/common/async'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; +import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; +import { CELL_OUTPUT_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { NotebookDiffEditorEventDispatcher, NotebookDiffLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; -export const IN_NOTEBOOK_TEXT_DIFF_EDITOR = new RawContextKey('isInNotebookTextDiffEditor', false); +const $ = DOM.$; export class NotebookTextDiffEditor extends EditorPane implements INotebookTextDiffEditor { static readonly ID: string = 'workbench.editor.notebookTextDiffEditor'; @@ -46,21 +52,38 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD private _rootElement!: HTMLElement; private _overflowContainer!: HTMLElement; private _dimension: DOM.Dimension | null = null; - private _list!: WorkbenchList; + private _diffElementViewModels: DiffElementViewModelBase[] = []; + private _list!: NotebookTextDiffList; + private _modifiedWebview: BackLayerWebView | null = null; + private _originalWebview: BackLayerWebView | null = null; + private _webviewTransparentCover: HTMLElement | null = null; private _fontInfo: BareFontInfo | undefined; - private readonly _onMouseUp = this._register(new Emitter<{ readonly event: MouseEvent; readonly target: CellDiffViewModel; }>()); + private readonly _onMouseUp = this._register(new Emitter<{ readonly event: MouseEvent; readonly target: DiffElementViewModelBase; }>()); public readonly onMouseUp = this._onMouseUp.event; private _eventDispatcher: NotebookDiffEditorEventDispatcher | undefined; protected _scopeContextKeyService!: IContextKeyService; private _model: INotebookDiffEditorModel | null = null; private _modifiedResourceDisposableStore = new DisposableStore(); + private _outputRenderer: OutputRenderer; get textModel() { return this._model?.modified.notebook; } private _revealFirst: boolean; + private readonly _insetModifyQueueByOutputId = new SequencerByKey(); + + protected _onDidDynamicOutputRendered = new Emitter<{ cell: IGenericCellViewModel, output: IDisplayOutputViewModel }>(); + onDidDynamicOutputRendered = this._onDidDynamicOutputRendered.event; + + private _localStore: DisposableStore = this._register(new DisposableStore()); + + private _isDisposed: boolean = false; + + get isDisposed() { + return this._isDisposed; + } constructor( @IInstantiationService readonly instantiationService: IInstantiationService, @@ -75,10 +98,40 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD ) { super(NotebookTextDiffEditor.ID, telemetryService, themeService, storageService); const editorOptions = this.configurationService.getValue('editor'); - this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); + this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel(), getPixelRatio()); this._revealFirst = true; this._register(this._modifiedResourceDisposableStore); + this._outputRenderer = new OutputRenderer(this, this.instantiationService); + } + + focusNotebookCell(cell: IGenericCellViewModel, focus: 'output' | 'editor' | 'container'): void { + // throw new Error('Method not implemented.'); + } + + focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'output' | 'editor' | 'container'): void { + // throw new Error('Method not implemented.'); + } + + updateOutputHeight(cellInfo: IDiffCellInfo, output: IDisplayOutputViewModel, outputHeight: number, isInit: boolean): void { + const diffElement = cellInfo.diffElement; + const cell = this.getCellByInfo(cellInfo); + const outputIndex = cell.outputsViewModels.indexOf(output); + + if (diffElement instanceof SideBySideDiffElementViewModel) { + const info = CellUri.parse(cellInfo.cellUri); + if (!info) { + return; + } + + diffElement.updateOutputHeight(info.notebook.toString() === this._model?.original.resource.toString() ? DiffSide.Original : DiffSide.Modified, outputIndex, outputHeight); + } else { + diffElement.updateOutputHeight(diffElement.type === 'insert' ? DiffSide.Modified : DiffSide.Original, outputIndex, outputHeight); + } + + if (isInit) { + this._onDidDynamicOutputRendered.fire({ cell, output }); + } } protected createEditor(parent: HTMLElement): void { @@ -87,16 +140,17 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._overflowContainer.classList.add('notebook-overflow-widget-container', 'monaco-editor'); DOM.append(parent, this._overflowContainer); - const renderer = this.instantiationService.createInstance(CellDiffRenderer, this); + const renderers = [ + this.instantiationService.createInstance(CellDiffSingleSideRenderer, this), + this.instantiationService.createInstance(CellDiffSideBySideRenderer, this), + ]; this._list = this.instantiationService.createInstance( NotebookTextDiffList, 'NotebookTextDiff', this._rootElement, this.instantiationService.createInstance(NotebookCellTextDiffListDelegate), - [ - renderer - ], + renderers, this.contextKeyService, { setRowLineHeight: false, @@ -140,17 +194,94 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } ); + this._register(this._list); + this._register(this._list.onMouseUp(e => { if (e.element) { this._onMouseUp.fire({ event: e.browserEvent, target: e.element }); } })); + + // transparent cover + this._webviewTransparentCover = DOM.append(this._list.rowsContainer, $('.webview-cover')); + this._webviewTransparentCover.style.display = 'none'; + + this._register(DOM.addStandardDisposableGenericMouseDownListner(this._overflowContainer, (e: StandardMouseEvent) => { + if (e.target.classList.contains('slider') && this._webviewTransparentCover) { + this._webviewTransparentCover.style.display = 'block'; + } + })); + + this._register(DOM.addStandardDisposableGenericMouseUpListner(this._overflowContainer, () => { + if (this._webviewTransparentCover) { + // no matter when + this._webviewTransparentCover.style.display = 'none'; + } + })); + + this._register(this._list.onDidScroll(e => { + this._webviewTransparentCover!.style.top = `${e.scrollTop}px`; + })); + + + } + + private _updateOutputsOffsetsInWebview(scrollTop: number, scrollHeight: number, activeWebview: BackLayerWebView, getActiveNestedCell: (diffElement: DiffElementViewModelBase) => DiffNestedCellViewModel | undefined, diffSide: DiffSide) { + activeWebview.element.style.height = `${scrollHeight}px`; + + if (activeWebview.insetMapping) { + const updateItems: IDisplayOutputLayoutUpdateRequest[] = []; + const removedItems: IDisplayOutputViewModel[] = []; + activeWebview.insetMapping.forEach((value, key) => { + const cell = getActiveNestedCell(value.cellInfo.diffElement); + if (!cell) { + return; + } + + const viewIndex = this._list.indexOf(value.cellInfo.diffElement); + + if (viewIndex === undefined) { + return; + } + + if (cell.outputsViewModels.indexOf(key) < 0) { + // output is already gone + removedItems.push(key); + } else { + const cellTop = this._list.getAbsoluteTopOfElement(value.cellInfo.diffElement); + if (activeWebview.shouldUpdateInset(cell, key, cellTop)) { + const outputIndex = cell.outputsViewModels.indexOf(key); + const outputOffset = cellTop + value.cellInfo.diffElement.getOutputOffsetInCell(diffSide, outputIndex); + + updateItems.push({ + output: key, + cellTop: cellTop, + outputOffset: outputOffset + }); + } + } + + }); + + removedItems.forEach(output => activeWebview.removeInset(output)); + + if (updateItems.length) { + activeWebview.updateViewScrollTop(-scrollTop, false, updateItems); + } + } } async setInput(input: NotebookDiffEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); - this._model = await input.resolve(); + const model = await input.resolve(); + if (this._model !== model) { + this._detachModel(); + this._model = model; + this._attachModel(); + } + + this._model = model; if (this._model === null) { return; } @@ -196,11 +327,73 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } })); - - this._eventDispatcher = new NotebookDiffEditorEventDispatcher(); + await this._createOriginalWebview(generateUuid(), this._model.original.resource); + await this._createModifiedWebview(generateUuid(), this._model.modified.resource); await this.updateLayout(); } + private _detachModel() { + this._localStore.clear(); + this._originalWebview?.dispose(); + this._originalWebview?.element.remove(); + this._originalWebview = null; + this._modifiedWebview?.dispose(); + this._modifiedWebview?.element.remove(); + this._modifiedWebview = null; + + this._modifiedResourceDisposableStore.clear(); + this._list.clear(); + + } + private _attachModel() { + this._eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const updateInsets = () => { + DOM.scheduleAtNextAnimationFrame(() => { + if (this._isDisposed) { + return; + } + + if (this._modifiedWebview) { + this._updateOutputsOffsetsInWebview(this._list.scrollTop, this._list.scrollHeight, this._modifiedWebview, (diffElement: DiffElementViewModelBase) => { + return diffElement.modified; + }, DiffSide.Modified); + } + + if (this._originalWebview) { + this._updateOutputsOffsetsInWebview(this._list.scrollTop, this._list.scrollHeight, this._originalWebview, (diffElement: DiffElementViewModelBase) => { + return diffElement.original; + }, DiffSide.Original); + } + }); + }; + + this._localStore.add(this._list.onDidChangeContentHeight(() => { + updateInsets(); + })); + + this._localStore.add(this._eventDispatcher.onDidChangeCellLayout(() => { + updateInsets(); + })); + } + + private async _createModifiedWebview(id: string, resource: URI): Promise { + this._modifiedWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, { outputNodePadding: CELL_OUTPUT_PADDING, outputNodeLeftPadding: 32 }) as BackLayerWebView; + // attach the webview container to the DOM tree first + this._list.rowsContainer.insertAdjacentElement('afterbegin', this._modifiedWebview.element); + await this._modifiedWebview.createWebview(); + this._modifiedWebview.element.style.width = `calc(50% - 16px)`; + this._modifiedWebview.element.style.left = `calc(50%)`; + } + + private async _createOriginalWebview(id: string, resource: URI): Promise { + this._originalWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, { outputNodePadding: CELL_OUTPUT_PADDING, outputNodeLeftPadding: 32 }) as BackLayerWebView; + // attach the webview container to the DOM tree first + this._list.rowsContainer.insertAdjacentElement('afterbegin', this._originalWebview.element); + await this._originalWebview.createWebview(); + this._originalWebview.element.style.width = `calc(50% - 16px)`; + this._originalWebview.element.style.left = `16px`; + } + private async _resolveStats(resource: URI) { if (resource.scheme === Schemas.untitled) { return undefined; @@ -222,7 +415,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD const diffResult = await this.notebookEditorWorkerService.computeDiff(this._model.original.resource, this._model.modified.resource); const cellChanges = diffResult.cellsDiff.changes; - const cellDiffViewModels: CellDiffViewModel[] = []; + const diffElementViewModels: DiffElementViewModelBase[] = []; const originalModel = this._model.original.notebook; const modifiedModel = this._model.modified.notebook; let originalCellIndex = 0; @@ -238,20 +431,24 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD const originalCell = originalModel.cells[originalCellIndex + j]; const modifiedCell = modifiedModel.cells[modifiedCellIndex + j]; if (originalCell.getHashValue() === modifiedCell.getHashValue()) { - cellDiffViewModels.push(new CellDiffViewModel( - originalCell, - modifiedCell, + diffElementViewModels.push(new SideBySideDiffElementViewModel( + this._model.modified.notebook, + this._model.original.notebook, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalCell), + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedCell), 'unchanged', this._eventDispatcher! )); } else { if (firstChangeIndex === -1) { - firstChangeIndex = cellDiffViewModels.length; + firstChangeIndex = diffElementViewModels.length; } - cellDiffViewModels.push(new CellDiffViewModel( - originalCell, - modifiedCell, + diffElementViewModels.push(new SideBySideDiffElementViewModel( + this._model.modified.notebook, + this._model.original.notebook, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalCell), + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedCell), 'modified', this._eventDispatcher! )); @@ -260,24 +457,35 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD const modifiedLCS = this._computeModifiedLCS(change, originalModel, modifiedModel); if (modifiedLCS.length && firstChangeIndex === -1) { - firstChangeIndex = cellDiffViewModels.length; + firstChangeIndex = diffElementViewModels.length; } - cellDiffViewModels.push(...modifiedLCS); + diffElementViewModels.push(...modifiedLCS); originalCellIndex = change.originalStart + change.originalLength; modifiedCellIndex = change.modifiedStart + change.modifiedLength; } for (let i = originalCellIndex; i < originalModel.cells.length; i++) { - cellDiffViewModels.push(new CellDiffViewModel( - originalModel.cells[i], - modifiedModel.cells[i - originalCellIndex + modifiedCellIndex], + diffElementViewModels.push(new SideBySideDiffElementViewModel( + this._model.modified.notebook, + this._model.original.notebook, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalModel.cells[i]), + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedModel.cells[i - originalCellIndex + modifiedCellIndex]), 'unchanged', this._eventDispatcher! )); } - this._list.splice(0, this._list.length, cellDiffViewModels); + this._originalWebview?.insetMapping.forEach((value, key) => { + this._originalWebview?.removeInset(key); + }); + + this._modifiedWebview?.insetMapping.forEach((value, key) => { + this._modifiedWebview?.removeInset(key); + }); + + this._diffElementViewModels = diffElementViewModels; + this._list.splice(0, this._list.length, diffElementViewModels); if (this._revealFirst && firstChangeIndex !== -1) { this._revealFirst = false; @@ -287,14 +495,16 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } private _computeModifiedLCS(change: IDiffChange, originalModel: NotebookTextModel, modifiedModel: NotebookTextModel) { - const result: CellDiffViewModel[] = []; + const result: DiffElementViewModelBase[] = []; // modified cells const modifiedLen = Math.min(change.originalLength, change.modifiedLength); for (let j = 0; j < modifiedLen; j++) { - result.push(new CellDiffViewModel( - originalModel.cells[change.originalStart + j], - modifiedModel.cells[change.modifiedStart + j], + result.push(new SideBySideDiffElementViewModel( + modifiedModel, + originalModel, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalModel.cells[change.originalStart + j]), + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedModel.cells[change.modifiedStart + j]), 'modified', this._eventDispatcher! )); @@ -302,8 +512,10 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD for (let j = modifiedLen; j < change.originalLength; j++) { // deletion - result.push(new CellDiffViewModel( - originalModel.cells[change.originalStart + j], + result.push(new SingleSideDiffElementViewModel( + originalModel, + modifiedModel, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalModel.cells[change.originalStart + j]), undefined, 'delete', this._eventDispatcher! @@ -312,9 +524,11 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD for (let j = modifiedLen; j < change.modifiedLength; j++) { // insertion - result.push(new CellDiffViewModel( + result.push(new SingleSideDiffElementViewModel( + modifiedModel, + originalModel, undefined, - modifiedModel.cells[change.modifiedStart + j], + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedModel.cells[change.modifiedStart + j]), 'insert', this._eventDispatcher! )); @@ -323,14 +537,12 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD return result; } - private pendingLayouts = new WeakMap(); + private pendingLayouts = new WeakMap(); - layoutNotebookCell(cell: CellDiffViewModel, height: number) { - const relayout = (cell: CellDiffViewModel, height: number) => { - const viewIndex = this._list.indexOf(cell); - - this._list?.updateElementHeight(viewIndex, height); + layoutNotebookCell(cell: DiffElementViewModelBase, height: number) { + const relayout = (cell: DiffElementViewModelBase, height: number) => { + this._list.updateElementHeight2(cell, height); }; if (this.pendingLayouts.has(cell)) { @@ -353,6 +565,79 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD return new Promise(resolve => { r = resolve; }); } + triggerScroll(event: IMouseWheelEvent) { + this._list.triggerScrollFromMouseWheelEvent(event); + } + + createInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, output: IInsetRenderOutput, getOffset: () => number, diffSide: DiffSide): void { + this._insetModifyQueueByOutputId.queue(output.source.model.outputId + (diffSide === DiffSide.Modified ? '-right' : 'left'), async () => { + const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; + if (!activeWebview) { + return; + } + + if (!activeWebview.insetMapping.has(output.source)) { + const cellTop = this._list.getAbsoluteTopOfElement(cellDiffViewModel); + await activeWebview.createInset({ diffElement: cellDiffViewModel, cellHandle: cellViewModel.handle, cellId: cellViewModel.id, cellUri: cellViewModel.uri }, output, cellTop, getOffset()); + } else { + const cellTop = this._list.getAbsoluteTopOfElement(cellDiffViewModel); + const scrollTop = this._list.scrollTop; + const outputIndex = cellViewModel.outputsViewModels.indexOf(output.source); + const outputOffset = cellTop + cellDiffViewModel.getOutputOffsetInCell(diffSide, outputIndex); + activeWebview.updateViewScrollTop(-scrollTop, true, [{ output: output.source, cellTop, outputOffset }]); + } + }); + } + + getCellByInfo(cellInfo: IDiffCellInfo): IGenericCellViewModel { + return cellInfo.diffElement.getCellByUri(cellInfo.cellUri); + } + + removeInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, displayOutput: IDisplayOutputViewModel, diffSide: DiffSide) { + this._insetModifyQueueByOutputId.queue(displayOutput.model.outputId + (diffSide === DiffSide.Modified ? '-right' : 'left'), async () => { + const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; + if (!activeWebview) { + return; + } + + if (!activeWebview.insetMapping.has(displayOutput)) { + return; + } + + activeWebview.removeInset(displayOutput); + }); + } + + showInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, displayOutput: IDisplayOutputViewModel, diffSide: DiffSide) { + this._insetModifyQueueByOutputId.queue(displayOutput.model.outputId + (diffSide === DiffSide.Modified ? '-right' : 'left'), async () => { + const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; + if (!activeWebview) { + return; + } + + if (!activeWebview.insetMapping.has(displayOutput)) { + return; + } + + const cellTop = this._list.getAbsoluteTopOfElement(cellDiffViewModel); + const scrollTop = this._list.scrollTop; + const outputIndex = cellViewModel.outputsViewModels.indexOf(displayOutput); + const outputOffset = cellTop + cellDiffViewModel.getOutputOffsetInCell(diffSide, outputIndex); + activeWebview.updateViewScrollTop(-scrollTop, true, [{ output: displayOutput, cellTop, outputOffset }]); + }); + } + + hideInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, output: IDisplayOutputViewModel) { + this._modifiedWebview?.hideInset(output); + this._originalWebview?.hideInset(output); + } + + // private async _resolveWebview(rightEditor: boolean): Promise { + // if (rightEditor) { + + // } + // } + getDomNode() { return this._rootElement; } @@ -380,6 +665,18 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._list?.splice(0, this._list?.length || 0); } + getOutputRenderer(): OutputRenderer { + return this._outputRenderer; + } + + deltaCellOutputContainerClassNames(diffSide: DiffSide, cellId: string, added: string[], removed: string[]) { + if (diffSide === DiffSide.Original) { + this._originalWebview?.deltaCellOutputContainerClassNames(cellId, added, removed); + } else { + this._modifiedWebview?.deltaCellOutputContainerClassNames(cellId, added, removed); + } + } + getLayoutInfo(): NotebookLayoutInfo { if (!this._list) { throw new Error('Editor is not initalized successfully'); @@ -392,6 +689,56 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }; } + getCellOutputLayoutInfo(nestedCell: DiffNestedCellViewModel) { + if (!this._model) { + throw new Error('Editor is not attached to model yet'); + } + const documentModel = CellUri.parse(nestedCell.uri); + if (!documentModel) { + throw new Error('Nested cell in the diff editor has wrong Uri'); + } + + const belongToOriginalDocument = this._model.original.notebook.uri.toString() === documentModel.notebook.toString(); + const viewModel = this._diffElementViewModels.find(element => { + const textModel = belongToOriginalDocument ? element.original : element.modified; + if (!textModel) { + return false; + } + + if (textModel.uri.toString() === nestedCell.uri.toString()) { + return true; + } + + return false; + }); + + if (!viewModel) { + throw new Error('Nested cell in the diff editor does not match any diff element'); + } + + if (viewModel.type === 'unchanged') { + return this.getLayoutInfo(); + } + + if (viewModel.type === 'insert' || viewModel.type === 'delete') { + return { + width: this._dimension!.width / 2, + height: this._dimension!.height / 2, + fontInfo: this._fontInfo! + }; + } + + if (viewModel.checkIfOutputsModified()) { + return { + width: this._dimension!.width / 2, + height: this._dimension!.height / 2, + fontInfo: this._fontInfo! + }; + } else { + return this.getLayoutInfo(); + } + } + layout(dimension: DOM.Dimension): void { this._rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600); this._rootElement.classList.toggle('narrow-width', dimension.width < 600); @@ -399,14 +746,39 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._rootElement.style.height = `${dimension.height}px`; this._list?.layout(this._dimension.height, this._dimension.width); - this._eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + + + if (this._modifiedWebview) { + this._modifiedWebview.element.style.width = `calc(50% - 16px)`; + this._modifiedWebview.element.style.left = `calc(50%)`; + } + + if (this._originalWebview) { + this._originalWebview.element.style.width = `calc(50% - 16px)`; + this._originalWebview.element.style.left = `16px`; + } + + if (this._webviewTransparentCover) { + this._webviewTransparentCover.style.height = `${dimension.height}px`; + this._webviewTransparentCover.style.width = `${dimension.width}px`; + } + + this._eventDispatcher?.emit([new NotebookDiffLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + } + + dispose() { + this._isDisposed = true; + super.dispose(); } } registerThemingParticipant((theme, collector) => { const cellBorderColor = theme.getColor(notebookCellBorder); if (cellBorderColor) { - collector.addRule(`.notebook-text-diff-editor .cell-body { border: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-body .border-container .top-border { border-top: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-body .border-container .bottom-border { border-top: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-body .border-container .left-border { border-left: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-body .border-container .right-border { border-right: 1px solid ${cellBorderColor};}`); collector.addRule(`.notebook-text-diff-editor .cell-diff-editor-container .output-header-container, .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container { border-top: 1px solid ${cellBorderColor}; @@ -429,6 +801,13 @@ registerThemingParticipant((theme, collector) => { const added = theme.getColor(diffInserted); if (added) { + collector.addRule( + ` + .monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-right div.foreground { background-color: ${added}; } + .monaco-workbench .notebook-text-diff-editor .cell-body.right .output-info-container .output-view-container div.foreground { background-color: ${added}; } + .monaco-workbench .notebook-text-diff-editor .cell-body.right .output-info-container .output-view-container div.output-empty-view { background-color: ${added}; } + ` + ); collector.addRule(` .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container { background-color: ${added}; } .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container .monaco-editor .margin, @@ -460,7 +839,15 @@ registerThemingParticipant((theme, collector) => { ); } const removed = theme.getColor(diffRemoved); - if (added) { + if (removed) { + collector.addRule( + ` + .monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-left div.foreground { background-color: ${removed}; } + .monaco-workbench .notebook-text-diff-editor .cell-body.left .output-info-container .output-view-container div.foreground { background-color: ${removed}; } + .monaco-workbench .notebook-text-diff-editor .cell-body.left .output-info-container .output-view-container div.output-empty-view { background-color: ${removed}; } + + ` + ); collector.addRule(` .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container { background-color: ${removed}; } .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container .monaco-editor .margin, diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts index 6e664d46a..ce2bc9c6e 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts @@ -14,12 +14,20 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; -import { CellDiffRenderTemplate, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { DiffElementViewModelBase, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { isMacintosh } from 'vs/base/common/platform'; -import { DeletedCell, InsertCell, ModifiedCell } from 'vs/workbench/contrib/notebook/browser/diff/cellComponents'; +import { DeletedElement, fixedDiffEditorOptions, fixedEditorOptions, getOptimizedNestedCodeEditorWidgetOptions, InsertElement, ModifiedElement } from 'vs/workbench/contrib/notebook/browser/diff/diffComponents'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; -export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { +export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { // private readonly lineHeight: number; constructor( @@ -29,20 +37,28 @@ export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { - static readonly TEMPLATE_ID = 'cell_diff'; +export class CellDiffSingleSideRenderer implements IListRenderer { + static readonly TEMPLATE_ID = 'cell_diff_single'; constructor( readonly notebookEditor: INotebookTextDiffEditor, @@ -50,56 +66,228 @@ export class CellDiffRenderer implements IListRenderer implements IDisposable, IStyleController { +export class CellDiffSideBySideRenderer implements IListRenderer { + static readonly TEMPLATE_ID = 'cell_diff_side_by_side'; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IContextMenuService protected readonly contextMenuService: IContextMenuService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, + @IMenuService protected readonly menuService: IMenuService, + @IContextKeyService protected readonly contextKeyService: IContextKeyService, + @INotificationService protected readonly notificationService: INotificationService, + ) { } + + get templateId() { + return CellDiffSideBySideRenderer.TEMPLATE_ID; + } + + renderTemplate(container: HTMLElement): CellDiffSideBySideRenderTemplate { + const body = DOM.$('.cell-body'); + DOM.append(container, body); + const diffEditorContainer = DOM.$('.cell-diff-editor-container'); + DOM.append(body, diffEditorContainer); + + const sourceContainer = DOM.append(diffEditorContainer, DOM.$('.source-container')); + const { editor, editorContainer } = this._buildSourceEditor(sourceContainer); + + const inputToolbarContainer = DOM.append(sourceContainer, DOM.$('.editor-input-toolbar-container')); + const cellToolbarContainer = DOM.append(inputToolbarContainer, DOM.$('div.property-toolbar')); + const toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { + actionViewItemProvider: action => { + if (action instanceof MenuItemAction) { + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); + return item; + } + + return undefined; + } + }); + + const metadataHeaderContainer = DOM.append(diffEditorContainer, DOM.$('.metadata-header-container')); + const metadataInfoContainer = DOM.append(diffEditorContainer, DOM.$('.metadata-info-container')); + + const outputHeaderContainer = DOM.append(diffEditorContainer, DOM.$('.output-header-container')); + const outputInfoContainer = DOM.append(diffEditorContainer, DOM.$('.output-info-container')); + + const borderContainer = DOM.append(body, DOM.$('.border-container')); + const leftBorder = DOM.append(borderContainer, DOM.$('.left-border')); + const rightBorder = DOM.append(borderContainer, DOM.$('.right-border')); + const topBorder = DOM.append(borderContainer, DOM.$('.top-border')); + const bottomBorder = DOM.append(borderContainer, DOM.$('.bottom-border')); + + + return { + body, + container, + diffEditorContainer, + sourceEditor: editor, + editorContainer, + inputToolbarContainer, + toolbar, + metadataHeaderContainer, + metadataInfoContainer, + outputHeaderContainer, + outputInfoContainer, + leftBorder, + rightBorder, + topBorder, + bottomBorder, + elementDisposables: new DisposableStore() + }; + } + + private _buildSourceEditor(sourceContainer: HTMLElement) { + const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); + + const editor = this.instantiationService.createInstance(DiffEditorWidget, editorContainer, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + originalEditable: false, + ignoreTrimWhitespace: false, + automaticLayout: false, + dimension: { + height: 0, + width: 0 + } + }, { + originalEditor: getOptimizedNestedCodeEditorWidgetOptions(), + modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() + }); + + return { + editor, + editorContainer + }; + } + + renderElement(element: SideBySideDiffElementViewModel, index: number, templateData: CellDiffSideBySideRenderTemplate, height: number | undefined): void { + templateData.body.classList.remove('left', 'right', 'full'); + + switch (element.type) { + case 'unchanged': + templateData.elementDisposables.add(this.instantiationService.createInstance(ModifiedElement, this.notebookEditor, element, templateData)); + return; + case 'modified': + templateData.elementDisposables.add(this.instantiationService.createInstance(ModifiedElement, this.notebookEditor, element, templateData)); + return; + default: + break; + } + } + + disposeTemplate(templateData: CellDiffSideBySideRenderTemplate): void { + templateData.container.innerText = ''; + templateData.sourceEditor.dispose(); + templateData.toolbar?.dispose(); + } + + disposeElement(element: SideBySideDiffElementViewModel, index: number, templateData: CellDiffSideBySideRenderTemplate): void { + templateData.elementDisposables.clear(); + } +} + +export class NotebookTextDiffList extends WorkbenchList implements IDisposable, IStyleController { private styleElement?: HTMLStyleElement; + get rowsContainer(): HTMLElement { + return this.view.containerDomNode; + } + constructor( listUser: string, container: HTMLElement, - delegate: IListVirtualDelegate, - renderers: IListRenderer[], + delegate: IListVirtualDelegate, + renderers: IListRenderer[], contextKeyService: IContextKeyService, - options: IWorkbenchListOptions, + options: IWorkbenchListOptions, @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, @@ -107,6 +295,32 @@ export class NotebookTextDiffList extends WorkbenchList imple super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); } + getAbsoluteTopOfElement(element: DiffElementViewModelBase): number { + const index = this.indexOf(element); + // if (index === undefined || index < 0 || index >= this.length) { + // this._getViewIndexUpperBound(element); + // throw new ListError(this.listUser, `Invalid index ${index}`); + // } + + return this.view.elementTop(index); + } + + triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) { + this.view.triggerScrollFromMouseWheelEvent(browserEvent); + } + + clear() { + super.splice(0, this.length); + } + + + updateElementHeight2(element: DiffElementViewModelBase, size: number) { + const viewIndex = this.indexOf(element); + const focused = this.getFocus(); + + this.view.updateElementHeight(viewIndex, size, focused.length ? focused[0] : null); + } + style(styles: IListStyles) { const selectorSuffix = this.view.domId; if (!this.styleElement) { diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index c2b2830c4..09cb20b09 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -184,11 +184,15 @@ .monaco-workbench .notebookOverlay .output .multi-mimetype-output { position: absolute; top: 4px; - left: -26px; + left: -30px; width: 16px; height: 16px; cursor: pointer; - padding: 4px; + padding: 6px; +} + +.monaco-workbench .notebookOverlay .output pre { + margin: 4px 0; } .monaco-workbench .notebookOverlay .output .error_message { @@ -248,15 +252,17 @@ } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { + position: relative; box-sizing: border-box; } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part .codicon { - margin-top: 4px; - position: relative; - left: -23px; + position: absolute; + padding: 2px 6px; + left: -30px; + bottom: 0; cursor: pointer; - z-index: 27; /* Over drag handle */ + z-index: 29; /* Over drag handle and bottom cell toolbar */ } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.collapsed .notebook-folding-indicator, @@ -475,6 +481,10 @@ text-align: center; } +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .output-collapsed .execution-count-label { + bottom: 24px; +} + .monaco-workbench .notebookOverlay .cell .cell-editor-part { position: relative; } @@ -585,7 +595,7 @@ display: flex; align-items: center; justify-content: center; - z-index: 25; /* over the focus outline on the editor, below the title toolbar */ + z-index: 28; /* over the focus outline on the editor, below the title toolbar */ width: 100%; opacity: 0; transition: opacity 0.2s ease-in-out; @@ -598,8 +608,9 @@ .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:focus-within, .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:hover, -.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within, -.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:hover { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell-bottom-toolbar-container, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list:focus-within > .monaco-scrollable-element > .monaco-list-rows:not(:hover) > .monaco-list-row.focused .cell-bottom-toolbar-container, +.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within { opacity: 1; } @@ -863,3 +874,18 @@ .cell-contributed-items.cell-contributed-items-left { margin-left: 4px; } + +.monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar { + width: 10px; /* width of the entire scrollbar */ + height: 10px; +} + +.monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar-thumb { + height: 10px; + width: 10px; +} + +.monaco-workbench .notebookOverlay .output .output-plaintext { + margin: 4px 0; + overflow-x: auto; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index b3e9c30f7..9289de4d4 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -31,12 +31,12 @@ import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/noteb import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; import { CellKind, CellToolbarLocKey, CellUri, DisplayOrderKey, getCellUndoRedoComparisonKey, NotebookDocumentBackupData, NotebookEditorPriority, NotebookTextDiffEditorPreview, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CustomEditorsAssociations, customEditorsAssociationsSettingId } from 'vs/workbench/services/editor/common/editorOpenWith'; import { CustomEditorInfo } from 'vs/workbench/contrib/customEditor/common/customEditor'; -import { INotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, IN_NOTEBOOK_TEXT_DIFF_EDITOR, NotebookEditorOptions, NOTEBOOK_EDITOR_OPEN } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { INotebookEditorModelResolverService, NotebookModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; @@ -47,11 +47,15 @@ import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/comm import { NotebookEditorWorkerServiceImpl } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { NotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl'; +import { INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; +import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetServiceImpl'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { Event } from 'vs/base/common/event'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { getFormatedMetadataJSON } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; // Editor Contribution @@ -59,7 +63,7 @@ import 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; import 'vs/workbench/contrib/notebook/browser/contrib/find/findController'; import 'vs/workbench/contrib/notebook/browser/contrib/fold/folding'; import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting'; -import 'vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider'; +import 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider'; import 'vs/workbench/contrib/notebook/browser/contrib/status/editorStatus'; // import 'vs/workbench/contrib/notebook/browser/contrib/scm/scm'; @@ -204,17 +208,24 @@ Registry.as(EditorInputExtensions.EditorInputFactor ); export class NotebookContribution extends Disposable implements IWorkbenchContribution { + private _notebookEditorIsOpen: IContextKey; + private _notebookDiffEditorIsOpen: IContextKey; constructor( + @IContextKeyService private readonly contextKeyService: IContextKeyService, @IEditorService private readonly editorService: IEditorService, @INotebookService private readonly notebookService: INotebookService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IUndoRedoService undoRedoService: IUndoRedoService, ) { super(); + this._notebookEditorIsOpen = NOTEBOOK_EDITOR_OPEN.bindTo(this.contextKeyService); + this._notebookDiffEditorIsOpen = IN_NOTEBOOK_TEXT_DIFF_EDITOR.bindTo(this.contextKeyService); + this._register(undoRedoService.registerUriComparisonKeyComputer(CellUri.scheme, { getComparisonKey: (uri: URI): string => { return getCellUndoRedoComparisonKey(uri); @@ -224,12 +235,28 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri this._register(this.editorService.overrideOpenEditor({ getEditorOverrides: (resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined) => { - const currentEditorForResource = group?.editors.find(editor => isEqual(editor.resource, resource)); + const currentEditorsForResource = group && this.editorService.findEditors(resource, group); + const currentEditorForResource = currentEditorsForResource && currentEditorsForResource.length ? currentEditorsForResource[0] : undefined; const associatedEditors = distinct([ ...this.getUserAssociatedNotebookEditors(resource), ...this.getContributedEditors(resource) - ], editor => editor.id); + ], editor => editor.id).sort((a, b) => { + // if a content provider is exclusive, it has higher order + if (a.exclusive && b.exclusive) { + return 0; + } + + if (a.exclusive) { + return -1; + } + + if (b.exclusive) { + return 1; + } + + return 0; + }); return associatedEditors.map(info => { return { @@ -264,6 +291,19 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri this.notebookService.updateActiveNotebookEditor(null); } })); + + this.editorGroupService.whenRestored.then(() => this._updateContextKeys()); + this._register(this.editorService.onDidActiveEditorChange(() => this._updateContextKeys())); + this._register(this.editorService.onDidVisibleEditorsChange(() => this._updateContextKeys())); + + this._register(this.editorGroupService.onDidAddGroup(() => this._updateContextKeys())); + this._register(this.editorGroupService.onDidRemoveGroup(() => this._updateContextKeys())); + } + + private _updateContextKeys() { + const activeEditorPane = this.editorService.activeEditorPane as { isNotebookEditor?: boolean } | undefined; + this._notebookEditorIsOpen.set(!!activeEditorPane?.isNotebookEditor); + this._notebookDiffEditorIsOpen.set(this.editorService.activeEditorPane?.getId() === 'workbench.editor.notebookTextDiffEditor'); } getUserAssociatedEditors(resource: URI) { @@ -300,8 +340,70 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri return undefined; } - if (originalInput instanceof NotebookEditorInput) { - return undefined; + // Run reopen with ... + if (id) { + // from the editor tab context menu + if (originalInput instanceof NotebookEditorInput) { + if (originalInput.viewType === id) { + // reopen with the same type + return undefined; + } else { + return { + override: (async () => { + const notebookInput = NotebookEditorInput.create(this.instantiationService, originalInput.resource, originalInput.getName(), id); + const originalEditorIndex = group.getIndexOfEditor(originalInput); + + await group.closeEditor(originalInput); + originalInput.dispose(); + const newEditor = await group.openEditor(notebookInput, { ...options, index: originalEditorIndex, override: false }); + if (newEditor) { + return newEditor; + } else { + return undefined; + } + })() + }; + } + } else { + // from the file explorer + const existingEditors = this.editorService.findEditors(originalInput.resource, group).filter(editor => editor instanceof NotebookEditorInput) as NotebookEditorInput[]; + + if (existingEditors.length) { + // there are notebook editors with the same resource + + if (existingEditors.find(editor => editor.viewType === id)) { + return { override: this.editorService.openEditor(existingEditors.find(editor => editor.viewType === id)!, { ...options, override: false }, group) }; + } else { + return { + override: (async () => { + const firstEditor = existingEditors[0]!; + const originalEditorIndex = group.getIndexOfEditor(firstEditor); + + await group.closeEditor(firstEditor); + firstEditor.dispose(); + const notebookInput = NotebookEditorInput.create(this.instantiationService, originalInput.resource!, originalInput.getName(), id); + const newEditor = await group.openEditor(notebookInput, { ...options, index: originalEditorIndex, override: false }); + + if (newEditor) { + return newEditor; + } else { + return undefined; + } + })() + }; + } + } + } + } + + // Click on the editor tab + if (id === undefined && originalInput instanceof NotebookEditorInput) { + const existingEditors = this.editorService.findEditors(originalInput.resource, group).filter(editor => editor instanceof NotebookEditorInput && editor === originalInput); + + if (existingEditors.length) { + // same + return undefined; + } } if (originalInput instanceof NotebookDiffEditorInput) { @@ -323,10 +425,8 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri } if (id === undefined) { - const existingEditors = group.editors.filter(editor => - editor.resource - && isEqual(editor.resource, notebookUri) - && !(editor instanceof NotebookEditorInput) + const existingEditors = this.editorService.findEditors(notebookUri, group).filter(editor => + !(editor instanceof NotebookEditorInput) && !(editor instanceof NotebookDiffEditorInput) ); @@ -391,7 +491,7 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri return undefined; } - const existingEditors = group.editors.filter(editor => editor.resource && isEqual(editor.resource, notebookUri) && !(editor instanceof NotebookEditorInput)); + const existingEditors = this.editorService.findEditors(notebookUri, group).filter(editor => !(editor instanceof NotebookEditorInput)); if (existingEditors.length) { return undefined; @@ -462,7 +562,7 @@ class CellContentProvider implements ITextModelContentProvider { create: (defaultEOL) => { const newEOL = (defaultEOL === DefaultEndOfLine.CRLF ? '\r\n' : '\n'); (cell.textBuffer as ITextBuffer).setEOL(newEOL); - return cell.textBuffer as ITextBuffer; + return { textBuffer: cell.textBuffer as ITextBuffer, disposable: Disposable.None }; }, getFirstLineText: (limit: number) => { return cell.textBuffer.getLineContent(1).substr(0, limit); @@ -489,6 +589,61 @@ class CellContentProvider implements ITextModelContentProvider { } } +class CellMetadataContentProvider implements ITextModelContentProvider { + private readonly _registration: IDisposable; + + constructor( + @ITextModelService textModelService: ITextModelService, + @IModelService private readonly _modelService: IModelService, + @IModeService private readonly _modeService: IModeService, + @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, + ) { + this._registration = textModelService.registerTextModelContentProvider(Schemas.vscodeNotebookCellMetadata, this); + } + + dispose(): void { + this._registration.dispose(); + } + + async provideTextContent(resource: URI): Promise { + const existing = this._modelService.getModel(resource); + if (existing) { + return existing; + } + const data = CellUri.parseCellMetadataUri(resource); + // const data = parseCellUri(resource); + if (!data) { + return null; + } + + const ref = await this._notebookModelResolverService.resolve(data.notebook); + let result: ITextModel | null = null; + + const mode = this._modeService.create('json'); + + for (const cell of ref.object.notebook.cells) { + if (cell.handle === data.handle) { + const metadataSource = getFormatedMetadataJSON(ref.object.notebook, cell.metadata || {}, cell.language); + result = this._modelService.createModel( + metadataSource, + mode, + resource + ); + break; + } + } + + if (result) { + const once = result.onWillDispose(() => { + once.dispose(); + ref.dispose(); + }); + } + + return result; + } +} + class RegisterSchemasContribution extends Disposable implements IWorkbenchContribution { constructor() { super(); @@ -596,6 +751,7 @@ class NotebookFileTracker implements IWorkbenchContribution { const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContribution, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(CellContentProvider, LifecyclePhase.Starting); +workbenchContributionsRegistry.registerWorkbenchContribution(CellMetadataContentProvider, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(RegisterSchemasContribution, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookFileTracker, LifecyclePhase.Ready); @@ -603,6 +759,7 @@ registerSingleton(INotebookService, NotebookService); registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl); registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverService, true); registerSingleton(INotebookCellStatusBarService, NotebookCellStatusBarService, true); +registerSingleton(INotebookEditorWidgetService, NotebookEditorWidgetService, true); const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 1633a9f7b..9f624090c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -22,7 +22,7 @@ import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/outpu import { RunStateRenderer, TimerRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, IProcessedOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, IEditor, INotebookKernelInfo2, IInsetRenderOutput, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IProcessedOutput, NotebookCellMetadata, NotebookDocumentMetadata, IEditor, INotebookKernelInfo2, ICellRange, IOrderedMimeType, ITransformedDisplayOutputDto, INotebookRendererInfo, IErrorOutput, IStreamOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IMenu } from 'vs/platform/actions/common/actions'; @@ -32,6 +32,7 @@ import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; +//#region Context Keys export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); // Is Notebook @@ -39,12 +40,16 @@ export const NOTEBOOK_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', ' // Editor keys export const NOTEBOOK_EDITOR_FOCUSED = new RawContextKey('notebookEditorFocused', false); +export const NOTEBOOK_EDITOR_OPEN = new RawContextKey('notebookEditorOpen', false); export const NOTEBOOK_CELL_LIST_FOCUSED = new RawContextKey('notebookCellListFocused', false); export const NOTEBOOK_OUTPUT_FOCUSED = new RawContextKey('notebookOutputFocused', false); export const NOTEBOOK_EDITOR_EDITABLE = new RawContextKey('notebookEditable', true); export const NOTEBOOK_EDITOR_RUNNABLE = new RawContextKey('notebookRunnable', true); export const NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK = new RawContextKey('notebookExecuting', false); +// Diff Editor Keys +export const IN_NOTEBOOK_TEXT_DIFF_EDITOR = new RawContextKey('isInNotebookTextDiffEditor', false); + // Cell keys export const NOTEBOOK_VIEW_TYPE = new RawContextKey('notebookViewType', undefined); export const NOTEBOOK_CELL_TYPE = new RawContextKey('notebookCellType', undefined); // code, markdown @@ -57,14 +62,114 @@ export const NOTEBOOK_CELL_RUN_STATE = new RawContextKey('notebookCellRu export const NOTEBOOK_CELL_HAS_OUTPUTS = new RawContextKey('notebookCellHasOutputs', false); // bool export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey('notebookCellInputIsCollapsed', false); // bool export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); // bool +// Kernels +export const NOTEBOOK_HAS_MULTIPLE_KERNELS = new RawContextKey('notebookHasMultipleKernels', false); -// Shared commands +//#endregion + +//#region Shared commands export const EXPAND_CELL_CONTENT_COMMAND_ID = 'notebook.cell.expandCellContent'; export const EXECUTE_CELL_COMMAND_ID = 'notebook.cell.execute'; -// Kernels +//#endregion -export const NOTEBOOK_HAS_MULTIPLE_KERNELS = new RawContextKey('notebookHasMultipleKernels', false); +//#region Output related types +export const enum RenderOutputType { + None, + Html, + Extension +} + +export interface IRenderNoOutput { + type: RenderOutputType.None; + hasDynamicHeight: boolean; +} + +export interface IRenderPlainHtmlOutput { + type: RenderOutputType.Html; + source: IDisplayOutputViewModel; + htmlContent: string; + hasDynamicHeight: boolean; +} + +export interface IRenderOutputViaExtension { + type: RenderOutputType.Extension; + source: IDisplayOutputViewModel; + mimeType: string; + renderer: INotebookRendererInfo; +} + +export type IInsetRenderOutput = IRenderPlainHtmlOutput | IRenderOutputViaExtension; +export type IRenderOutput = IRenderNoOutput | IInsetRenderOutput; + +export const outputHasDynamicHeight = (o: IRenderOutput) => o.type !== RenderOutputType.Extension && o.hasDynamicHeight; + + +export interface ICellOutputViewModel { + cellViewModel: IGenericCellViewModel; + model: IProcessedOutput; + isDisplayOutput(): this is IDisplayOutputViewModel; + isErrorOutput(): this is IErrorOutputViewModel; + isStreamOutput(): this is IStreamOutputViewModel; +} + +export interface IDisplayOutputViewModel extends ICellOutputViewModel { + model: ITransformedDisplayOutputDto; + resolveMimeTypes(textModel: NotebookTextModel): [readonly IOrderedMimeType[], number]; + pickedMimeType: number; +} + +export interface IErrorOutputViewModel extends ICellOutputViewModel { + model: IErrorOutput; +} + +export interface IStreamOutputViewModel extends ICellOutputViewModel { + model: IStreamOutput; +} + +//#endregion + +//#region Shared types between the Notebook Editor and Notebook Diff Editor, they are mostly used for output rendering + +export interface IGenericCellViewModel { + id: string; + handle: number; + uri: URI; + metadata: NotebookCellMetadata | undefined; + outputIsHovered: boolean; + outputsViewModels: ICellOutputViewModel[]; + getOutputOffset(index: number): number; + updateOutputHeight(index: number, height: number): void; +} + +export interface IDisplayOutputLayoutUpdateRequest { + output: IDisplayOutputViewModel; + cellTop: number; + outputOffset: number; +} + +export interface ICommonCellInfo { + cellId: string; + cellHandle: number; + cellUri: URI; +} + +export interface INotebookCellOutputLayoutInfo { + width: number; + height: number; + fontInfo: BareFontInfo; +} + +export interface ICommonNotebookEditor { + getCellOutputLayoutInfo(cell: IGenericCellViewModel): INotebookCellOutputLayoutInfo; + triggerScroll(event: IMouseWheelEvent): void; + getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel; + focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; + focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; + updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean): void; +} + +//#endregion export interface NotebookLayoutInfo { width: number; @@ -122,7 +227,7 @@ export interface MarkdownCellLayoutChangeEvent { totalHeight?: number; } -export interface ICellViewModel { +export interface ICellViewModel extends IGenericCellViewModel { readonly model: NotebookCellTextModel; readonly id: string; readonly textBuffer: IReadonlyTextBuffer; @@ -133,6 +238,7 @@ export interface ICellViewModel { cellKind: CellKind; editState: CellEditState; focusMode: CellFocusMode; + outputIsHovered: boolean; getText(): string; getTextLength(): number; metadata: NotebookCellMetadata | undefined; @@ -212,7 +318,7 @@ export interface IActiveNotebookEditor extends INotebookEditor { uri: URI; } -export interface INotebookEditor extends IEditor { +export interface INotebookEditor extends IEditor, ICommonNotebookEditor { isEmbedded: boolean; cursorNavigationMode: boolean; @@ -323,6 +429,8 @@ export interface INotebookEditor extends IEditor { */ focusNotebookCell(cell: ICellViewModel, focus: 'editor' | 'container' | 'output'): void; + focusNextNotebookCell(cell: ICellViewModel, focus: 'editor' | 'container' | 'output'): void; + /** * Execute the given notebook cell */ @@ -361,12 +469,12 @@ export interface INotebookEditor extends IEditor { /** * Remove the output from the webview layer */ - removeInset(output: IProcessedOutput): void; + removeInset(output: IDisplayOutputViewModel): void; /** * Hide the inset in the webview layer without removing it */ - hideInset(output: IProcessedOutput): void; + hideInset(output: IDisplayOutputViewModel): void; /** * Send message to the webview for outputs. @@ -395,11 +503,21 @@ export interface INotebookEditor extends IEditor { */ triggerScroll(event: IMouseWheelEvent): void; + /** + * The range will be revealed with as little scrolling as possible. + */ + revealCellRangeInView(range: ICellRange): void; + /** * Reveal cell into viewport. */ revealInView(cell: ICellViewModel): void; + /** + * Reveal cell into the top of viewport. + */ + revealInViewAtTop(cell: ICellViewModel): void; + /** * Reveal cell into viewport center. */ @@ -476,6 +594,9 @@ export interface INotebookEditor extends IEditor { * @return The contribution or null if contribution not found. */ getContribution(id: string): T; + + getCellByInfo(cellInfo: ICommonCellInfo): ICellViewModel; + updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean): void; } export interface INotebookCellList { @@ -494,8 +615,8 @@ export interface INotebookCellList { scrollLeft: number; length: number; rowsContainer: HTMLElement; - readonly onDidRemoveOutput: Event; - readonly onDidHideOutput: Event; + readonly onDidRemoveOutput: Event; + readonly onDidHideOutput: Event; readonly onMouseUp: Event>; readonly onMouseDown: Event>; readonly onContextMenu: Event>; @@ -506,7 +627,9 @@ export interface INotebookCellList { focusElement(element: ICellViewModel): void; selectElement(element: ICellViewModel): void; getFocusedElements(): ICellViewModel[]; + revealElementsInView(range: ICellRange): void; revealElementInView(element: ICellViewModel): void; + revealElementInViewAtTop(element: ICellViewModel): void; revealElementInCenterIfOutsideViewport(element: ICellViewModel): void; revealElementInCenter(element: ICellViewModel): void; revealElementInCenterIfOutsideViewportAsync(element: ICellViewModel): Promise; @@ -594,7 +717,7 @@ export interface IOutputTransformContribution { * This call is allowed to have side effects, such as placing output * directly into the container element. */ - render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput; + render(output: ICellOutputViewModel, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput; } export interface CellFindMatch { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index d0e140358..1025ccaf6 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -20,13 +20,13 @@ import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorOptions, IEditorInput, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -import { IBorrowValue, INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; import { INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorDropService'; import { IEditorGroup, IEditorGroupsService, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { IBorrowValue, INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; @@ -64,14 +64,7 @@ export class NotebookEditor extends EditorPane { this._editorMemento = this.getEditorMemento(_editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); } - set viewModel(newModel: NotebookViewModel | undefined) { - if (this._widget.value) { - this._widget.value.viewModel = newModel; - this._onDidChangeModel.fire(); - } - } - - get viewModel() { + get viewModel(): NotebookViewModel | undefined { return this._widget.value?.viewModel; } @@ -134,17 +127,18 @@ export class NotebookEditor extends EditorPane { this._widget.value?.focus(); } + hasFocus(): boolean { + const activeElement = document.activeElement; + const value = this._widget.value; + + return !!value && (DOM.isAncestor(activeElement, value.getDomNode() || DOM.isAncestor(activeElement, value.getOverflowContainerDomNode()))); + } + async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const group = this.group!; this._saveEditorViewState(this.input); - await super.setInput(input, options, context, token); - - // Check for cancellation - if (token.isCancellationRequested) { - return undefined; - } this._widgetDisposableStore.clear(); @@ -155,11 +149,16 @@ export class NotebookEditor extends EditorPane { } this._widget = this.instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, input); + this._widgetDisposableStore.add(this._widget.value!.onDidChangeModel(() => this._onDidChangeModel.fire())); if (this._dimension) { this._widget.value!.layout(this._dimension, this._rootElement); } + // only now `setInput` and yield/await. this is AFTER the actual widget is ready. This is very important + // so that others synchronously receive a notebook editor with the correct widget being set + await super.setInput(input, options, context, token); + const model = await input.resolve(); // Check for cancellation if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index b58b545fe..90204097c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getZoomLevel } from 'vs/base/browser/browser'; +import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import * as strings from 'vs/base/common/strings'; import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -40,9 +40,8 @@ import { EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorMemento } from 'vs/workbench/common/editor'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; -import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugToolBar'; -import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellViewModel, INotebookCellList, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_OUTPUT_PADDING, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, INotebookCellList, INotebookCellOutputLayoutInfo, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { NotebookKernelProviderAssociation, NotebookKernelProviderAssociations, notebookKernelProviderAssociationsSettingId } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; @@ -55,13 +54,16 @@ import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, CellToolbarLocKey, ICellRange, IInsetRenderOutput, INotebookDecorationRenderOptions, INotebookKernelInfo2, IProcessedOutput, isTransformedDisplayOutput, NotebookCellRunState, NotebookRunState, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellToolbarLocKey, ICellRange, INotebookDecorationRenderOptions, INotebookKernelInfo2, NotebookCellRunState, NotebookRunState, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { configureKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { configureKernelIcon, errorStateIcon, successStateIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { extname } from 'vs/base/common/resources'; const $ = DOM.$; @@ -73,9 +75,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _overlayContainer!: HTMLElement; private _body!: HTMLElement; private _overflowContainer!: HTMLElement; - private _webview: BackLayerWebView | null = null; + private _webview: BackLayerWebView | null = null; private _webviewResolved: boolean = false; - private _webviewResolvePromise: Promise | null = null; + private _webviewResolvePromise: Promise | null> | null = null; private _webviewTransparentCover: HTMLElement | null = null; private _list!: INotebookCellList; private _dndController: CellDragAndDropController | null = null; @@ -146,6 +148,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._notebookViewModel?.notebookDocument; } + private _activeKernelExecuted: boolean = false; private _activeKernel: INotebookKernelInfo2 | undefined = undefined; private readonly _onDidChangeKernel = this._register(new Emitter()); readonly onDidChangeKernel: Event = this._onDidChangeKernel.event; @@ -153,6 +156,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor readonly onDidChangeAvailableKernels: Event = this._onDidChangeAvailableKernels.event; private _contributedKernelsComputePromise: CancelablePromise | null = null; + private _initialKernelComputationDone: boolean = false; get activeKernel() { return this._activeKernel; @@ -175,9 +179,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._activeKernelResolvePromise = undefined; const memento = this._activeKernelMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); - memento[this.viewModel.viewType] = this._activeKernel?.id; + memento[this.viewModel.viewType] = this._activeKernel?.friendlyId; this._activeKernelMemento.saveMemento(); this._onDidChangeKernel.fire(); + if (this._activeKernel) { + this._loadKernelPreloads(this._activeKernel.extensionLocation, this._activeKernel); + } } private _activeKernelResolvePromise: Promise | undefined = undefined; @@ -248,7 +255,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor @IContextMenuService private readonly contextMenuService: IContextMenuService, @IMenuService private readonly menuService: IMenuService, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IThemeService private readonly themeService: IThemeService + @IThemeService private readonly themeService: IThemeService, + @ITelemetryService private readonly telemetryService: ITelemetryService ) { super(); this.isEmbedded = creationOptions.isEmbedded || false; @@ -428,7 +436,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _generateFontInfo(): void { const editorOptions = this.configurationService.getValue('editor'); - this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); + this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel(), getPixelRatio()); } private _createBody(parent: HTMLElement): void { @@ -653,6 +661,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor if (this.viewModel === undefined || !this.viewModel.equal(textModel)) { this._detachModel(); await this._attachModel(textModel, viewState); + + type WorkbenchNotebookOpenClassification = { + scheme: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; + ext: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; + viewType: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; + }; + + type WorkbenchNotebookOpenEvent = { + scheme: string; + ext: string; + viewType: string; + }; + + this.telemetryService.publicLog2('notebook/editorOpened', { + scheme: textModel.uri.scheme, + ext: extname(textModel.uri), + viewType: textModel.viewType + }); } else { this.restoreListViewState(viewState); } @@ -730,6 +756,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._webview?.element.remove(); this._webview = null; this._list.clear(); + this._activeKernel = undefined; + this._activeKernelExecuted = false; } async beginComputeContributedKernels() { @@ -742,6 +770,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }); const result = await this._contributedKernelsComputePromise; + this._initialKernelComputationDone = true; this._contributedKernelsComputePromise = null; return result; @@ -752,6 +781,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } + if (this._activeKernel !== undefined && this._activeKernelExecuted) { + // kernel already executed, we should not change it automatically + return; + } + const provider = this.notebookService.getContributedNotebookProvider(this.viewModel.viewType) || this.notebookService.getContributedNotebookProviders(this.viewModel.uri)[0]; const availableKernels = await this.beginComputeContributedKernels(); @@ -771,7 +805,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.multipleKernelsAvailable = false; } - const activeKernelStillExist = [...availableKernels].find(kernel => kernel.id === this.activeKernel?.id && this.activeKernel?.id !== undefined); + const activeKernelStillExist = [...availableKernels].find(kernel => kernel.friendlyId === this.activeKernel?.friendlyId && this.activeKernel?.friendlyId !== undefined); if (activeKernelStillExist) { // the kernel still exist, we don't want to modify the selection otherwise user's temporary preference is lost @@ -782,6 +816,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._setKernelsFromProviders(provider, availableKernels, tokenSource); } + this._initialKernelComputationDone = true; + tokenSource.dispose(); } @@ -797,7 +833,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const cachedKernelId = memento[provider.id]; this.activeKernel = filteredKernels.find(kernel => kernel.isPreferred) - || filteredKernels.find(kernel => kernel.id === cachedKernelId) + || filteredKernels.find(kernel => kernel.friendlyId === cachedKernelId) || filteredKernels[0]; } else { this.activeKernel = undefined; @@ -818,7 +854,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - memento[provider.id] = this._activeKernel?.id; + memento[provider.id] = this._activeKernel?.friendlyId; this._activeKernelMemento.saveMemento(); tokenSource.dispose(); @@ -831,7 +867,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const cachedKernelId = memento[provider.id]; const preferedKernel = kernelsFromSameExtension.find(kernel => kernel.isPreferred) - || kernelsFromSameExtension.find(kernel => kernel.id === cachedKernelId) + || kernelsFromSameExtension.find(kernel => kernel.friendlyId === cachedKernelId) || kernelsFromSameExtension[0]; this.activeKernel = preferedKernel; if (this.activeKernel) { @@ -848,7 +884,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - memento[provider.id] = this._activeKernel?.id; + memento[provider.id] = this._activeKernel?.friendlyId; this._activeKernelMemento.saveMemento(); tokenSource.dispose(); return; @@ -892,7 +928,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._notebookExecuting?.set(notebookMetadata.runState === NotebookRunState.Running); } - private async _resolveWebview(): Promise { + private async _resolveWebview(): Promise | null> { if (!this.textModel) { return null; } @@ -902,7 +938,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } if (!this._webview) { - this._webview = this.instantiationService.createInstance(BackLayerWebView, this, this.getId(), this.textModel!.uri); + this._webview = this.instantiationService.createInstance(BackLayerWebView, this, this.getId(), this.textModel!.uri, { outputNodePadding: CELL_OUTPUT_PADDING, outputNodeLeftPadding: CELL_OUTPUT_PADDING }); + this._webview.element.style.width = `calc(100% - ${CODE_CELL_LEFT_MARGIN + (CELL_MARGIN * 2) + CELL_RUN_GUTTER}px)`; + this._webview.element.style.margin = `0px 0 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px`; + // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._webview.element); } @@ -950,7 +989,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } private async _createWebview(id: string, resource: URI): Promise { - this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource); + this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, { outputNodePadding: CELL_OUTPUT_PADDING, outputNodeLeftPadding: CELL_OUTPUT_PADDING }); + this._webview.element.style.width = `calc(100% - ${CODE_CELL_LEFT_MARGIN + (CELL_MARGIN * 2) + CELL_RUN_GUTTER}px)`; + this._webview.element.style.margin = `0px 0 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px`; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._webview.element); } @@ -1016,27 +1057,36 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._webview!.element.style.height = `${scrollHeight}px`; if (this._webview?.insetMapping) { - const updateItems: { cell: CodeCellViewModel, output: IProcessedOutput, cellTop: number }[] = []; - const removedItems: IProcessedOutput[] = []; + const updateItems: IDisplayOutputLayoutUpdateRequest[] = []; + const removedItems: IDisplayOutputViewModel[] = []; this._webview?.insetMapping.forEach((value, key) => { - const cell = value.cell; + const cell = this.viewModel?.getCellByHandle(value.cellInfo.cellHandle); + if (!cell || !(cell instanceof CodeCellViewModel)) { + return; + } + + this.viewModel?.viewCells.find(cell => cell.handle === value.cellInfo.cellHandle); const viewIndex = this._list.getViewIndex(cell); if (viewIndex === undefined) { return; } - if (cell.outputs.indexOf(key) < 0) { + if (cell.outputsViewModels.indexOf(key) < 0) { // output is already gone removedItems.push(key); } const cellTop = this._list.getAbsoluteTopOfElement(cell); if (this._webview!.shouldUpdateInset(cell, key, cellTop)) { + const outputIndex = cell.outputsViewModels.indexOf(key); + + const outputOffset = cellTop + cell.getOutputOffset(outputIndex); + updateItems.push({ - cell: cell, output: key, - cellTop: cellTop + cellTop: cellTop, + outputOffset }); } }); @@ -1212,10 +1262,18 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor // this.viewModel!.selectionHandles = [cell.handle]; } + revealCellRangeInView(range: ICellRange) { + return this._list.revealElementsInView(range); + } + revealInView(cell: ICellViewModel) { this._list.revealElementInView(cell); } + revealInViewAtTop(cell: ICellViewModel) { + this._list.revealElementInViewAtTop(cell); + } + revealInCenterIfOutsideViewport(cell: ICellViewModel) { this._list.revealElementInCenterIfOutsideViewport(cell); } @@ -1421,6 +1479,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor language = this.viewModel.resolvedLanguages[0] || 'plaintext'; } } + + if (this.viewModel.resolvedLanguages.indexOf(language) < 0) { + // the language no longer exists + language = this.viewModel.resolvedLanguages[0] || 'plaintext'; + } } else { language = 'markdown'; } @@ -1627,26 +1690,37 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private async _ensureActiveKernel() { if (this._activeKernel) { - if (this._activeKernelResolvePromise) { - await this._activeKernelResolvePromise; - } - return; } + if (this._activeKernelResolvePromise) { + await this._activeKernelResolvePromise; + + if (this._activeKernel) { + return; + } + } + + + if (!this._initialKernelComputationDone) { + await this._setKernels(new CancellationTokenSource()); + + if (this._activeKernel) { + return; + } + } + // pick active kernel const picker = this.quickInputService.createQuickPick<(IQuickPickItem & { run(): void; kernelProviderId?: string })>(); picker.placeholder = nls.localize('notebook.runCell.selectKernel', "Select a notebook kernel to run this notebook"); picker.matchOnDetail = true; - picker.busy = true; - picker.show(); const tokenSource = new CancellationTokenSource(); const availableKernels = await this.beginComputeContributedKernels(); const picks: QuickPickInput[] = availableKernels.map((a) => { return { - id: a.id, + id: a.friendlyId, label: a.label, picked: false, description: @@ -1736,6 +1810,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } await this._ensureActiveKernel(); + this._activeKernelExecuted = true; await this._activeKernel?.executeNotebookCell!(this.viewModel.uri, undefined); } @@ -1776,6 +1851,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } await this._ensureActiveKernel(); + this._activeKernelExecuted = true; await this._activeKernel?.executeNotebookCell!(this.viewModel.uri, cell.handle); } @@ -1818,6 +1894,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } + focusNextNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output') { + const idx = this.viewModel?.getCellIndex(cell); + if (typeof idx !== 'number') { + return; + } + + const newCell = this.viewModel?.viewCells[idx + 1]; + if (!newCell) { + return; + } + + this.focusNotebookCell(newCell, focusItem); + } + //#endregion //#region MISC @@ -1842,12 +1932,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }; } + getCellOutputLayoutInfo(cell: IGenericCellViewModel): INotebookCellOutputLayoutInfo { + if (!this._list) { + throw new Error('Editor is not initalized successfully'); + } + + return { + width: this._dimension!.width, + height: this._dimension!.height, + fontInfo: this._fontInfo! + }; + } + triggerScroll(event: IMouseWheelEvent) { this._list.triggerScrollFromMouseWheelEvent(event); } async createInset(cell: CodeCellViewModel, output: IInsetRenderOutput, offset: number): Promise { - this._insetModifyQueueByOutputId.queue(output.source.outputId, async () => { + this._insetModifyQueueByOutputId.queue(output.source.model.outputId, async () => { if (!this._webview) { return; } @@ -1856,22 +1958,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor if (!this._webview!.insetMapping.has(output.source)) { const cellTop = this._list.getAbsoluteTopOfElement(cell); - await this._webview!.createInset(cell, output, cellTop, offset); + await this._webview!.createInset({ cellId: cell.id, cellHandle: cell.handle, cellUri: cell.uri }, output, cellTop, offset); } else { const cellTop = this._list.getAbsoluteTopOfElement(cell); const scrollTop = this._list.scrollTop; + const outputIndex = cell.outputsViewModels.indexOf(output.source); + const outputOffset = cellTop + cell.getOutputOffset(outputIndex); - this._webview!.updateViewScrollTop(-scrollTop, true, [{ cell, output: output.source, cellTop }]); + this._webview!.updateViewScrollTop(-scrollTop, true, [{ output: output.source, cellTop, outputOffset }]); } }); } - removeInset(output: IProcessedOutput) { - if (!isTransformedDisplayOutput(output)) { + removeInset(output: ICellOutputViewModel) { + if (!output.isDisplayOutput()) { return; } - this._insetModifyQueueByOutputId.queue(output.outputId, async () => { + this._insetModifyQueueByOutputId.queue(output.model.outputId, async () => { if (!this._webview || !this._webviewResolved) { return; } @@ -1879,16 +1983,16 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }); } - hideInset(output: IProcessedOutput) { + hideInset(output: ICellOutputViewModel) { if (!this._webview || !this._webviewResolved) { return; } - if (!isTransformedDisplayOutput(output)) { + if (!output.isDisplayOutput()) { return; } - this._insetModifyQueueByOutputId.queue(output.outputId, async () => { + this._insetModifyQueueByOutputId.queue(output.model.outputId, async () => { this._webview!.hideInset(output); }); } @@ -1921,6 +2025,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._overlayContainer.classList.remove(className); } + getCellByInfo(cellInfo: ICommonCellInfo): ICellViewModel { + const { cellHandle } = cellInfo; + return this.viewModel?.viewCells.find(vc => vc.handle === cellHandle) as CodeCellViewModel; + } + + updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, outputHeight: number, isInit: boolean): void { + const cell = this.viewModel?.viewCells.find(vc => vc.handle === cellInfo.cellHandle); + if (cell && cell instanceof CodeCellViewModel) { + const outputIndex = cell.outputsViewModels.indexOf(output); + cell.updateOutputHeight(outputIndex, outputHeight); + this.layoutNotebookCell(cell, cell.layoutInfo.totalHeight); + } + } + //#endregion @@ -2022,7 +2140,7 @@ export const selectedCellBorder = registerColor('notebook.selectedCellBorder', { dark: notebookCellBorder, light: notebookCellBorder, hc: contrastBorder -}, nls.localize('notebook.selectedCellBorder', "The color of the cell's top and bottom border when the cell is selected but not focusd.")); +}, nls.localize('notebook.selectedCellBorder', "The color of the cell's top and bottom border when the cell is selected but not focused.")); export const focusedCellBorder = registerColor('notebook.focusedCellBorder', { dark: focusBorder, @@ -2030,6 +2148,12 @@ export const focusedCellBorder = registerColor('notebook.focusedCellBorder', { hc: focusBorder }, nls.localize('notebook.focusedCellBorder', "The color of the cell's top and bottom border when the cell is focused.")); +export const inactiveFocusedCellBorder = registerColor('notebook.inactiveFocusedCellBorder', { + dark: notebookCellBorder, + light: notebookCellBorder, + hc: notebookCellBorder +}, nls.localize('notebook.inactiveFocusedCellBorder', "The color of the cell's top and bottom border when a cell is focused while the primary focus is outside of the editor.")); + export const cellStatusBarItemHover = registerColor('notebook.cellStatusBarItemHoverBackground', { light: new Color(new RGBA(0, 0, 0, 0.08)), dark: new Color(new RGBA(255, 255, 255, 0.15)), @@ -2042,7 +2166,6 @@ export const cellInsertionIndicator = registerColor('notebook.cellInsertionIndic hc: focusBorder }, nls.localize('notebook.cellInsertionIndicator', "The color of the notebook cell insertion indicator.")); - export const listScrollbarSliderBackground = registerColor('notebookScrollbarSlider.background', { dark: scrollbarSliderBackground, light: scrollbarSliderBackground, @@ -2161,6 +2284,15 @@ registerThemingParticipant((theme, collector) => { border-color: ${focusedCellBorderColor} !important; }`); + const inactiveFocusedBorderColor = theme.getColor(inactiveFocusedCellBorder); + collector.addRule(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-top:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-bottom:before, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row.focused .cell-inner-container:not(.cell-editor-focus):before, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row.focused .cell-inner-container:not(.cell-editor-focus):after { + border-color: ${inactiveFocusedBorderColor} !important; + }`); + const selectedCellBorderColor = theme.getColor(selectedCellBorder); collector.addRule(` .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-focus-indicator-top:before, @@ -2191,12 +2323,12 @@ registerThemingParticipant((theme, collector) => { const cellStatusSuccessIcon = theme.getColor(cellStatusIconSuccess); if (cellStatusSuccessIcon) { - collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status .codicon-notebook-state-success { color: ${cellStatusSuccessIcon} }`); + collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status ${ThemeIcon.asCSSSelector(successStateIcon)} { color: ${cellStatusSuccessIcon} }`); } const cellStatusErrorIcon = theme.getColor(cellStatusIconError); if (cellStatusErrorIcon) { - collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status .codicon-notebook-state-error { color: ${cellStatusErrorIcon} }`); + collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status ${ThemeIcon.asCSSSelector(errorStateIcon)} { color: ${cellStatusErrorIcon} }`); } const cellStatusRunningIcon = theme.getColor(cellStatusIconRunning); @@ -2219,12 +2351,14 @@ registerThemingParticipant((theme, collector) => { if (scrollbarSliderBackgroundColor) { collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider { background: ${editorBackgroundColor}; } `); collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider:before { content: ""; width: 100%; height: 100%; position: absolute; background: ${scrollbarSliderBackgroundColor}; } `); /* hack to not have cells see through scroller */ + // collector.addRule(` .monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar-track { background: ${scrollbarSliderBackgroundColor}; } `); } const scrollbarSliderHoverBackgroundColor = theme.getColor(listScrollbarSliderHoverBackground); if (scrollbarSliderHoverBackgroundColor) { collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider:hover { background: ${editorBackgroundColor}; } `); collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider:hover:before { content: ""; width: 100%; height: 100%; position: absolute; background: ${scrollbarSliderHoverBackgroundColor}; } `); /* hack to not have cells see through scroller */ + collector.addRule(` .monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar-thumb { background: ${scrollbarSliderHoverBackgroundColor}; } `); } const scrollbarSliderActiveBackgroundColor = theme.getColor(listScrollbarSliderActiveBackground); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts index f9650ad09..ce817ad43 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts @@ -3,14 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ResourceMap } from 'vs/base/common/map'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { IEditorGroupsService, IEditorGroup, GroupChangeKind, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IInstantiationService, createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export const INotebookEditorWidgetService = createDecorator('INotebookEditorWidgetService'); @@ -20,139 +16,6 @@ export interface IBorrowValue { export interface INotebookEditorWidgetService { _serviceBrand: undefined; + widgets: NotebookEditorWidget[]; retrieveWidget(accessor: ServicesAccessor, group: IEditorGroup, input: NotebookEditorInput): IBorrowValue; } - -class NotebookEditorWidgetService implements INotebookEditorWidgetService { - - readonly _serviceBrand: undefined; - - private _tokenPool = 1; - - private readonly _notebookWidgets = new Map>(); - private readonly _disposables = new DisposableStore(); - - constructor( - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IEditorService editorService: IEditorService, - ) { - - const groupListener = new Map(); - const onNewGroup = (group: IEditorGroup) => { - const { id } = group; - const listener = group.onDidGroupChange(e => { - const widgets = this._notebookWidgets.get(group.id); - if (!widgets || e.kind !== GroupChangeKind.EDITOR_CLOSE || !(e.editor instanceof NotebookEditorInput)) { - return; - } - const value = widgets.get(e.editor.resource); - if (!value) { - return; - } - value.token = undefined; - this._disposeWidget(value.widget); - widgets.delete(e.editor.resource); - }); - groupListener.set(id, listener); - }; - this._disposables.add(editorGroupService.onDidAddGroup(onNewGroup)); - editorGroupService.groups.forEach(onNewGroup); - - // group removed -> clean up listeners, clean up widgets - this._disposables.add(editorGroupService.onDidRemoveGroup(group => { - const listener = groupListener.get(group.id); - if (listener) { - listener.dispose(); - groupListener.delete(group.id); - } - const widgets = this._notebookWidgets.get(group.id); - this._notebookWidgets.delete(group.id); - if (widgets) { - for (const value of widgets.values()) { - value.token = undefined; - this._disposeWidget(value.widget); - } - } - })); - - // HACK - // we use the open override to spy on tab movements because that's the only - // way to do that... - this._disposables.add(editorService.overrideOpenEditor({ - open: (input, _options, group, context) => { - if (input instanceof NotebookEditorInput && context === OpenEditorContext.MOVE_EDITOR) { - // when moving a notebook editor we release it from its current tab and we - // "place" it into its future slot so that the editor can pick it up from there - this._freeWidget(input, editorGroupService.activeGroup, group); - } - return undefined; - } - })); - } - - private _disposeWidget(widget: NotebookEditorWidget): void { - widget.onWillHide(); - const domNode = widget.getDomNode(); - widget.dispose(); - domNode.remove(); - } - - private _freeWidget(input: NotebookEditorInput, source: IEditorGroup, target: IEditorGroup): void { - const targetWidget = this._notebookWidgets.get(target.id)?.get(input.resource); - if (targetWidget) { - // not needed - return; - } - - const widget = this._notebookWidgets.get(source.id)?.get(input.resource); - if (!widget) { - throw new Error('no widget at source group'); - } - this._notebookWidgets.get(source.id)?.delete(input.resource); - widget.token = undefined; - - let targetMap = this._notebookWidgets.get(target.id); - if (!targetMap) { - targetMap = new ResourceMap(); - this._notebookWidgets.set(target.id, targetMap); - } - targetMap.set(input.resource, widget); - } - - retrieveWidget(accessor: ServicesAccessor, group: IEditorGroup, input: NotebookEditorInput): IBorrowValue { - - let value = this._notebookWidgets.get(group.id)?.get(input.resource); - - if (!value) { - // NEW widget - const instantiationService = accessor.get(IInstantiationService); - const widget = instantiationService.createInstance(NotebookEditorWidget, { isEmbedded: false }); - const token = this._tokenPool++; - value = { widget, token }; - - let map = this._notebookWidgets.get(group.id); - if (!map) { - map = new ResourceMap(); - this._notebookWidgets.set(group.id, map); - } - map.set(input.resource, value); - - } else { - // reuse a widget which was either free'ed before or which - // is simply being reused... - value.token = this._tokenPool++; - } - - return this._createBorrowValue(value.token!, value); - } - - private _createBorrowValue(myToken: number, widget: { widget: NotebookEditorWidget, token: number | undefined }): IBorrowValue { - return { - get value() { - return widget.token === myToken ? widget.widget : undefined; - } - }; - } -} - -registerSingleton(INotebookEditorWidgetService, NotebookEditorWidgetService, true); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetServiceImpl.ts new file mode 100644 index 000000000..f365cdfdf --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetServiceImpl.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ResourceMap } from 'vs/base/common/map'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { IEditorGroupsService, IEditorGroup, GroupChangeKind, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IBorrowValue, INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; + +export class NotebookEditorWidgetService implements INotebookEditorWidgetService { + + readonly _serviceBrand: undefined; + + private _tokenPool = 1; + + private readonly _notebookWidgets = new Map>(); + private readonly _disposables = new DisposableStore(); + + get widgets() { + return [...this._notebookWidgets.values()].map(val => [...val.values()].map(widget => widget.widget)).reduce((prev, curr) => { return [...prev, ...curr]; }, []); + } + + constructor( + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IEditorService editorService: IEditorService, + ) { + + const groupListener = new Map(); + const onNewGroup = (group: IEditorGroup) => { + const { id } = group; + const listener = group.onDidGroupChange(e => { + const widgets = this._notebookWidgets.get(group.id); + if (!widgets || e.kind !== GroupChangeKind.EDITOR_CLOSE || !(e.editor instanceof NotebookEditorInput)) { + return; + } + const value = widgets.get(e.editor.resource); + if (!value) { + return; + } + value.token = undefined; + this._disposeWidget(value.widget); + widgets.delete(e.editor.resource); + }); + groupListener.set(id, listener); + }; + this._disposables.add(editorGroupService.onDidAddGroup(onNewGroup)); + editorGroupService.groups.forEach(onNewGroup); + + // group removed -> clean up listeners, clean up widgets + this._disposables.add(editorGroupService.onDidRemoveGroup(group => { + const listener = groupListener.get(group.id); + if (listener) { + listener.dispose(); + groupListener.delete(group.id); + } + const widgets = this._notebookWidgets.get(group.id); + this._notebookWidgets.delete(group.id); + if (widgets) { + for (const value of widgets.values()) { + value.token = undefined; + this._disposeWidget(value.widget); + } + } + })); + + // HACK + // we use the open override to spy on tab movements because that's the only + // way to do that... + this._disposables.add(editorService.overrideOpenEditor({ + open: (input, _options, group, context) => { + if (input instanceof NotebookEditorInput && context === OpenEditorContext.MOVE_EDITOR) { + // when moving a notebook editor we release it from its current tab and we + // "place" it into its future slot so that the editor can pick it up from there + this._freeWidget(input, editorGroupService.activeGroup, group); + } + return undefined; + } + })); + } + + private _disposeWidget(widget: NotebookEditorWidget): void { + widget.onWillHide(); + const domNode = widget.getDomNode(); + widget.dispose(); + domNode.remove(); + } + + private _freeWidget(input: NotebookEditorInput, source: IEditorGroup, target: IEditorGroup): void { + const targetWidget = this._notebookWidgets.get(target.id)?.get(input.resource); + if (targetWidget) { + // not needed + return; + } + + const widget = this._notebookWidgets.get(source.id)?.get(input.resource); + if (!widget) { + throw new Error('no widget at source group'); + } + this._notebookWidgets.get(source.id)?.delete(input.resource); + widget.token = undefined; + + let targetMap = this._notebookWidgets.get(target.id); + if (!targetMap) { + targetMap = new ResourceMap(); + this._notebookWidgets.set(target.id, targetMap); + } + targetMap.set(input.resource, widget); + } + + retrieveWidget(accessor: ServicesAccessor, group: IEditorGroup, input: NotebookEditorInput): IBorrowValue { + + let value = this._notebookWidgets.get(group.id)?.get(input.resource); + + if (!value) { + // NEW widget + const instantiationService = accessor.get(IInstantiationService); + const widget = instantiationService.createInstance(NotebookEditorWidget, { isEmbedded: false }); + const token = this._tokenPool++; + value = { widget, token }; + + let map = this._notebookWidgets.get(group.id); + if (!map) { + map = new ResourceMap(); + this._notebookWidgets.set(group.id, map); + } + map.set(input.resource, value); + + } else { + // reuse a widget which was either free'ed before or which + // is simply being reused... + value.token = this._tokenPool++; + } + + return this._createBorrowValue(value.token!, value); + } + + private _createBorrowValue(myToken: number, widget: { widget: NotebookEditorWidget, token: number | undefined }): IBorrowValue { + return { + get value() { + return widget.token === myToken ? widget.widget : undefined; + } + }; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts index 6780f1fa9..0ff8cae8f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts @@ -29,4 +29,5 @@ export const collapsedIcon = registerIcon('notebook-collapsed', Codicon.chevronR export const expandedIcon = registerIcon('notebook-expanded', Codicon.chevronDown, localize('expandedIcon', 'Icon to annotated a expanded section in notebook editors.')); export const openAsTextIcon = registerIcon('notebook-open-as-text', Codicon.fileCode, localize('openAsTextIcon', 'Icon to open the notebook in a text editor.')); export const revertIcon = registerIcon('notebook-revert', Codicon.discard, localize('revertIcon', 'Icon to revert in notebook editors.')); +export const renderOutputIcon = registerIcon('notebook-render-output', Codicon.preview, localize('renderOutputIcon', 'Icon to render output in diff editor.')); export const mimetypeIcon = registerIcon('notebook-mimetype', Codicon.code, localize('mimetypeIcon', 'Icon for a mime type in notebook editors.')); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts b/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts index abf2220cc..e97746c12 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts @@ -5,9 +5,9 @@ import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { BrandedService, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICommonNotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -export type IOutputTransformCtor = IConstructorSignature1; +export type IOutputTransformCtor = IConstructorSignature1; export interface IOutputTransformDescription { id: string; @@ -20,7 +20,7 @@ export const NotebookRegistry = new class NotebookRegistryImpl { readonly outputTransforms: IOutputTransformDescription[] = []; - registerOutputTransform(id: string, kind: CellOutputKind, ctor: { new(editor: INotebookEditor, ...services: Services): IOutputTransformContribution }): void { + registerOutputTransform(id: string, kind: CellOutputKind, ctor: { new(editor: ICommonNotebookEditor, ...services: Services): IOutputTransformContribution }): void { this.outputTransforms.push({ id: id, kind: kind, ctor: ctor as IOutputTransformCtor }); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index ec00de561..6209a04e5 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getZoomLevel } from 'vs/base/browser/browser'; +import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; import { flatten } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -65,6 +65,10 @@ export class NotebookKernelProviderInfoStore extends Disposable { return this._notebookKernelProviders.filter(provider => notebookDocumentFilterMatch(provider.selector, viewType, resource)); } + getContributedKernelProviders() { + return [...this._notebookKernelProviders.values()]; + } + private _updateProviderExtensionsInfo() { NotebookKernelProviderAssociationRegistry.extensionIds.length = 0; NotebookKernelProviderAssociationRegistry.extensionDescriptions.length = 0; @@ -237,7 +241,7 @@ class ModelData implements IDisposable { } export class NotebookService extends Disposable implements INotebookService, ICustomEditorViewTypesHandler { declare readonly _serviceBrand: undefined; - private readonly _notebookProviders = new Map(); + private readonly _notebookProviders = new Map(); notebookProviderInfoStore: NotebookProviderInfoStore; notebookRenderersInfoStore: NotebookOutputRendererInfoStore = new NotebookOutputRendererInfoStore(); notebookKernelProviderInfoStore: NotebookKernelProviderInfoStore = new NotebookKernelProviderInfoStore(); @@ -266,12 +270,12 @@ export class NotebookService extends Disposable implements INotebookService, ICu private readonly _onDidChangeKernels = new Emitter(); onDidChangeKernels: Event = this._onDidChangeKernels.event; - private readonly _onDidChangeNotebookActiveKernel = new Emitter<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }>(); - onDidChangeNotebookActiveKernel: Event<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }> = this._onDidChangeNotebookActiveKernel.event; + private readonly _onDidChangeNotebookActiveKernel = new Emitter<{ uri: URI, providerHandle: number | undefined, kernelFriendlyId: string | undefined; }>(); + onDidChangeNotebookActiveKernel: Event<{ uri: URI, providerHandle: number | undefined, kernelFriendlyId: string | undefined; }> = this._onDidChangeNotebookActiveKernel.event; private cutItems: NotebookCellTextModel[] | undefined; private _lastClipboardIsCopy: boolean = true; - private _displayOrder: { userOrder: string[], defaultOrder: string[] } = Object.create(null); + private _displayOrder: { userOrder: string[], defaultOrder: string[]; } = Object.create(null); private readonly _decorationOptionProviders = new Map(); constructor( @@ -366,7 +370,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu // there is a `::before` or `::after` text decoration whose position is above or below current line // we at least make sure that the editor top padding is at least one line const editorOptions = this.configurationService.getValue('editor'); - updateEditorTopPadding(BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()).lineHeight + 2); + updateEditorTopPadding(BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel(), getPixelRatio()).lineHeight + 2); decorationTriggeredAdjustment = true; break; } @@ -387,7 +391,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu }; }; - const PRIORITY = 50; + const PRIORITY = 105; this._register(UndoCommand.addImplementation(PRIORITY, () => { const { editor } = getContext(); if (editor?.viewModel) { @@ -623,6 +627,8 @@ export class NotebookService extends Disposable implements INotebookService, ICu } async canResolve(viewType: string): Promise { + await this._extensionService.activateByEvent(`onNotebook:*`); + if (!this._notebookProviders.has(viewType)) { await this._extensionService.whenInstalledExtensionsRegistered(); // this awaits full activation of all matching extensions @@ -691,9 +697,10 @@ export class NotebookService extends Disposable implements INotebookService, ICu const data = await provider.provideKernels(resource, token); result[index] = data.map(dto => { return { + id: dto.id, extension: dto.extension, extensionLocation: URI.revive(dto.extensionLocation), - id: dto.id, + friendlyId: dto.friendlyId, label: dto.label, description: dto.description, detail: dto.detail, @@ -701,13 +708,13 @@ export class NotebookService extends Disposable implements INotebookService, ICu preloads: dto.preloads, providerHandle: dto.providerHandle, resolve: async (uri: URI, editorId: string, token: CancellationToken) => { - return provider.resolveKernel(editorId, uri, dto.id, token); + return provider.resolveKernel(editorId, uri, dto.friendlyId, token); }, executeNotebookCell: async (uri: URI, handle: number | undefined) => { - return provider.executeNotebook(uri, dto.id, handle); + return provider.executeNotebook(uri, dto.friendlyId, handle); }, cancelNotebookCell: (uri: URI, handle: number | undefined): Promise => { - return provider.cancelNotebook(uri, dto.id, handle); + return provider.cancelNotebook(uri, dto.friendlyId, handle); } }; }); @@ -718,6 +725,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu return flatten(result); } + async getContributedNotebookKernelProviders(): Promise { + const kernelProviders = this.notebookKernelProviderInfoStore.getContributedKernelProviders(); + return kernelProviders; + } + getRendererInfo(id: string): INotebookRendererInfo | undefined { return this.notebookRenderersInfoStore.get(id); } @@ -890,7 +902,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu listVisibleNotebookEditors(): INotebookEditor[] { return this._editorService.visibleEditorPanes - .filter(pane => (pane as unknown as { isNotebookEditor?: boolean }).isNotebookEditor) + .filter(pane => (pane as unknown as { isNotebookEditor?: boolean; }).isNotebookEditor) .map(pane => pane.getControl() as INotebookEditor) .filter(editor => !!editor) .filter(editor => this._notebookEditors.has(editor.getId())); @@ -912,7 +924,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu this._onDidChangeNotebookActiveKernel.fire({ uri: editor.uri!, providerHandle: editor.activeKernel?.providerHandle, - kernelId: editor.activeKernel?.id + kernelFriendlyId: editor.activeKernel?.friendlyId }); })); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index d6d8ed7c0..8d0c6a542 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -19,9 +19,9 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED, cellRangesEqual } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED, cellRangesEqual, ICellOutputViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { diff, IProcessedOutput, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, ICellRange, NOTEBOOK_EDITOR_CURSOR_BEGIN_END } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { diff, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, ICellRange, NOTEBOOK_EDITOR_CURSOR_BEGIN_END } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { clamp } from 'vs/base/common/numbers'; import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; @@ -45,10 +45,10 @@ export class NotebookCellList extends WorkbenchList implements ID private _viewModelStore = new DisposableStore(); private styleElement?: HTMLStyleElement; - private readonly _onDidRemoveOutput = new Emitter(); - readonly onDidRemoveOutput: Event = this._onDidRemoveOutput.event; - private readonly _onDidHideOutput = new Emitter(); - readonly onDidHideOutput: Event = this._onDidHideOutput.event; + private readonly _onDidRemoveOutput = new Emitter(); + readonly onDidRemoveOutput: Event = this._onDidRemoveOutput.event; + private readonly _onDidHideOutput = new Emitter(); + readonly onDidHideOutput: Event = this._onDidHideOutput.event; private _viewModel: NotebookViewModel | null = null; private _hiddenRangeIds: string[] = []; @@ -314,15 +314,17 @@ export class NotebookCellList extends WorkbenchList implements ID if (e.synchronous) { viewDiffs.reverse().forEach((diff) => { // remove output in the webview - const hideOutputs: IProcessedOutput[] = []; - const deletedOutputs: IProcessedOutput[] = []; + const hideOutputs: ICellOutputViewModel[] = []; + const deletedOutputs: ICellOutputViewModel[] = []; for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell.handle)) { - hideOutputs.push(...cell?.model.outputs); - } else { - deletedOutputs.push(...cell?.model.outputs); + if (cell.cellKind === CellKind.Code) { + if (this._viewModel!.hasCell(cell.handle)) { + hideOutputs.push(...cell?.outputsViewModels); + } else { + deletedOutputs.push(...cell?.outputsViewModels); + } } } @@ -338,15 +340,17 @@ export class NotebookCellList extends WorkbenchList implements ID } viewDiffs.reverse().forEach((diff) => { - const hideOutputs: IProcessedOutput[] = []; - const deletedOutputs: IProcessedOutput[] = []; + const hideOutputs: ICellOutputViewModel[] = []; + const deletedOutputs: ICellOutputViewModel[] = []; for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell.handle)) { - hideOutputs.push(...cell?.model.outputs); - } else { - deletedOutputs.push(...cell?.model.outputs); + if (cell.cellKind === CellKind.Code) { + if (this._viewModel!.hasCell(cell.handle)) { + hideOutputs.push(...cell?.outputsViewModels); + } else { + deletedOutputs.push(...cell?.outputsViewModels); + } } } @@ -460,15 +464,17 @@ export class NotebookCellList extends WorkbenchList implements ID viewDiffs.reverse().forEach((diff) => { // remove output in the webview - const hideOutputs: IProcessedOutput[] = []; - const deletedOutputs: IProcessedOutput[] = []; + const hideOutputs: ICellOutputViewModel[] = []; + const deletedOutputs: ICellOutputViewModel[] = []; for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell.handle)) { - hideOutputs.push(...cell?.model.outputs); - } else { - deletedOutputs.push(...cell?.model.outputs); + if (cell.cellKind === CellKind.Code) { + if (this._viewModel!.hasCell(cell.handle)) { + hideOutputs.push(...cell?.outputsViewModels); + } else { + deletedOutputs.push(...cell?.outputsViewModels); + } } } @@ -541,6 +547,22 @@ export class NotebookCellList extends WorkbenchList implements ID return viewIndexInfo.index; } + private _getViewIndexUpperBound2(modelIndex: number) { + if (!this.hiddenRangesPrefixSum) { + return modelIndex; + } + + const viewIndexInfo = this.hiddenRangesPrefixSum.getIndexOf(modelIndex); + + if (viewIndexInfo.remainder !== 0) { + if (modelIndex >= this.hiddenRangesPrefixSum.getTotalValue()) { + return modelIndex - (this.hiddenRangesPrefixSum.getTotalValue() - this.hiddenRangesPrefixSum.getCount()); + } + } + + return viewIndexInfo.index; + } + focusElement(cell: ICellViewModel) { const index = this._getViewIndexUpperBound(cell); @@ -581,6 +603,46 @@ export class NotebookCellList extends WorkbenchList implements ID super.setFocus(indexes, browserEvent); } + revealElementsInView(range: ICellRange) { + const startIndex = this._getViewIndexUpperBound2(range.start); + + if (startIndex < 0) { + return; + } + + const endIndex = this._getViewIndexUpperBound2(range.end); + + const scrollTop = this.getViewScrollTop(); + const wrapperBottom = this.getViewScrollBottom(); + const elementTop = this.view.elementTop(startIndex); + if (elementTop >= scrollTop + && elementTop < wrapperBottom) { + // start element is visible + // check end + + const endElementTop = this.view.elementTop(endIndex); + const endElementHeight = this.view.elementHeight(endIndex); + + if (endElementTop >= wrapperBottom) { + return this._revealInternal(startIndex, false, CellRevealPosition.Top); + } + + if (endElementTop < wrapperBottom) { + // end element partially visible + if (endElementTop + endElementHeight - wrapperBottom < elementTop - scrollTop) { + // there is enough space to just scroll up a little bit to make the end element visible + return this.view.setScrollTop(scrollTop + endElementTop + endElementHeight - wrapperBottom); + } else { + // don't even try it + return this._revealInternal(startIndex, false, CellRevealPosition.Top); + } + } + } + + + this._revealInView(startIndex); + } + revealElementInView(cell: ICellViewModel) { const index = this._getViewIndexUpperBound(cell); @@ -589,6 +651,14 @@ export class NotebookCellList extends WorkbenchList implements ID } } + revealElementInViewAtTop(cell: ICellViewModel) { + const index = this._getViewIndexUpperBound(cell); + + if (index >= 0) { + this._revealInternal(index, false, CellRevealPosition.Top); + } + } + revealElementInCenterIfOutsideViewport(cell: ICellViewModel) { const index = this._getViewIndexUpperBound(cell); diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts index 41a457e3d..39da33ff4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IProcessedOutput, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellOutputViewModel, ICommonNotebookEditor, IOutputTransformContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { URI } from 'vs/base/common/uri'; export class OutputRenderer { @@ -15,7 +14,7 @@ export class OutputRenderer { protected readonly _mimeTypeMapping: { [key: number]: IOutputTransformContribution; }; constructor( - notebookEditor: INotebookEditor, + notebookEditor: ICommonNotebookEditor, private readonly instantiationService: IInstantiationService ) { this._contributions = {}; @@ -34,7 +33,8 @@ export class OutputRenderer { } } - renderNoop(output: IProcessedOutput, container: HTMLElement): IRenderOutput { + renderNoop(viewModel: ICellOutputViewModel, container: HTMLElement): IRenderOutput { + const output = viewModel.model; const contentNode = document.createElement('p'); contentNode.innerText = `No renderer could be found for output. It has the following output type: ${output.outputKind}`; @@ -42,13 +42,14 @@ export class OutputRenderer { return { type: RenderOutputType.None, hasDynamicHeight: false }; } - render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput { + render(viewModel: ICellOutputViewModel, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput { + const output = viewModel.model; const transform = this._mimeTypeMapping[output.outputKind]; if (transform) { - return transform.render(output, container, preferredMimeType, notebookUri); + return transform.render(viewModel, container, preferredMimeType, notebookUri); } else { - return this.renderNoop(output, container); + return this.renderNoop(viewModel, container); } } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts index 10a4839b4..8a0e7216d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts @@ -3,21 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRenderOutput, CellOutputKind, IErrorOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import { RGBA, Color } from 'vs/base/common/color'; import { ansiColorIdentifiers } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICommonNotebookEditor, IErrorOutputViewModel, IOutputTransformContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; class ErrorTransform implements IOutputTransformContribution { constructor( - public editor: INotebookEditor, + public editor: ICommonNotebookEditor, @IThemeService private readonly themeService: IThemeService ) { } - render(output: IErrorOutput, container: HTMLElement): IRenderOutput { + render(viewModel: IErrorOutputViewModel, container: HTMLElement): IRenderOutput { + const output = viewModel.model; const header = document.createElement('div'); const headerMessage = output.ename && output.evalue ? `${output.ename}: ${output.evalue}` diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts index b2cec3995..420f309b4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRenderOutput, CellOutputKind, ITransformedDisplayOutputDto, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import * as DOM from 'vs/base/browser/dom'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICommonNotebookEditor, IDisplayOutputViewModel, IOutputTransformContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { isArray } from 'vs/base/common/types'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -22,10 +22,10 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; class RichRenderer implements IOutputTransformContribution { - private _richMimeTypeRenderers = new Map IRenderOutput>(); + private _richMimeTypeRenderers = new Map IRenderOutput>(); constructor( - public notebookEditor: INotebookEditor, + public notebookEditor: ICommonNotebookEditor, @IInstantiationService private readonly instantiationService: IInstantiationService, @IModelService private readonly modelService: IModelService, @IModeService private readonly modeService: IModeService, @@ -44,8 +44,8 @@ class RichRenderer implements IOutputTransformContribution { this._richMimeTypeRenderers.set('text/x-javascript', this.renderCode.bind(this)); } - render(output: ITransformedDisplayOutputDto, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI): IRenderOutput { - if (!output.data) { + render(output: IDisplayOutputViewModel, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI): IRenderOutput { + if (!output.model.data) { const contentNode = document.createElement('p'); contentNode.innerText = `No data could be found for output.`; container.appendChild(contentNode); @@ -55,7 +55,7 @@ class RichRenderer implements IOutputTransformContribution { if (!preferredMimeType || !this._richMimeTypeRenderers.has(preferredMimeType)) { const contentNode = document.createElement('p'); const mimeTypes = []; - for (const property in output.data) { + for (const property in output.model.data) { mimeTypes.push(property); } @@ -75,8 +75,8 @@ class RichRenderer implements IOutputTransformContribution { return renderer!(output, notebookUri, container); } - renderJSON(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['application/json']; + renderJSON(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['application/json']; const str = JSON.stringify(data, null, '\t'); const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { @@ -94,8 +94,8 @@ class RichRenderer implements IOutputTransformContribution { const textModel = this.modelService.createModel(str, mode, resource, false); editor.setModel(textModel); - const width = this.notebookEditor.getLayoutInfo().width; - const fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; + const width = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).width; + const fontInfo = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).fontInfo; const height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); editor.layout({ @@ -108,8 +108,8 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderCode(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['text/x-javascript']; + renderCode(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['text/x-javascript']; const str = (isArray(data) ? data.join('') : data) as string; const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { @@ -127,8 +127,8 @@ class RichRenderer implements IOutputTransformContribution { const textModel = this.modelService.createModel(str, mode, resource, false); editor.setModel(textModel); - const width = this.notebookEditor.getLayoutInfo().width; - const fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; + const width = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).width; + const fontInfo = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).fontInfo; const height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); editor.layout({ @@ -141,8 +141,8 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderJavaScript(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['application/javascript']; + renderJavaScript(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['application/javascript']; const str = isArray(data) ? data.join('') : data; const scriptVal = ``; return { @@ -153,8 +153,8 @@ class RichRenderer implements IOutputTransformContribution { }; } - renderHTML(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['text/html']; + renderHTML(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['text/html']; const str = (isArray(data) ? data.join('') : data) as string; return { type: RenderOutputType.Html, @@ -164,8 +164,8 @@ class RichRenderer implements IOutputTransformContribution { }; } - renderSVG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['image/svg+xml']; + renderSVG(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['image/svg+xml']; const str = (isArray(data) ? data.join('') : data) as string; return { type: RenderOutputType.Html, @@ -175,8 +175,8 @@ class RichRenderer implements IOutputTransformContribution { }; } - renderMarkdown(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['text/markdown']; + renderMarkdown(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['text/markdown']; const str = (isArray(data) ? data.join('') : data) as string; const mdOutput = document.createElement('div'); const mdRenderer = this.instantiationService.createInstance(MarkdownRenderer, { baseUrl: dirname(notebookUri) }); @@ -186,9 +186,9 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderPNG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { + renderPNG(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { const image = document.createElement('img'); - image.src = `data:image/png;base64,${output.data['image/png']}`; + image.src = `data:image/png;base64,${output.model.data['image/png']}`; const display = document.createElement('div'); display.classList.add('display'); display.appendChild(image); @@ -196,9 +196,9 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderJPEG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { + renderJPEG(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { const image = document.createElement('img'); - image.src = `data:image/jpeg;base64,${output.data['image/jpeg']}`; + image.src = `data:image/jpeg;base64,${output.model.data['image/jpeg']}`; const display = document.createElement('div'); display.classList.add('display'); display.appendChild(image); @@ -206,8 +206,8 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderPlainText(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['text/plain']; + renderPlainText(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['text/plain']; const contentNode = DOM.$('.output-plaintext'); truncatedArrayOfString(contentNode, isArray(data) ? data : [data], this.openerService, this.textFileService, this.themeService, true); container.appendChild(contentNode); diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts index b05a25d3b..5f6f9b773 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { IRenderOutput, CellOutputKind, IStreamOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICommonNotebookEditor, IOutputTransformContribution, IRenderOutput, IStreamOutputViewModel, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { truncatedArrayOfString } from 'vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -14,14 +14,15 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; class StreamRenderer implements IOutputTransformContribution { constructor( - editor: INotebookEditor, + editor: ICommonNotebookEditor, @IOpenerService readonly openerService: IOpenerService, @ITextFileService readonly textFileService: ITextFileService, @IThemeService readonly themeService: IThemeService ) { } - render(output: IStreamOutput, container: HTMLElement): IRenderOutput { + render(viewModel: IStreamOutputViewModel, container: HTMLElement): IRenderOutput { + const output = viewModel.model; const contentNode = DOM.$('.output-stream'); truncatedArrayOfString(contentNode, [output.text], this.openerService, this.textFileService, this.themeService, false); container.appendChild(contentNode); diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts index 8ef36f850..c8dc77318 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts @@ -61,14 +61,14 @@ export function truncatedArrayOfString(container: HTMLElement, outputs: string[] const bufferBuilder = new PieceTreeTextBufferBuilder(); outputs.forEach(output => bufferBuilder.acceptChunk(output)); const factory = bufferBuilder.finish(); - buffer = factory.create(DefaultEndOfLine.LF); + buffer = factory.create(DefaultEndOfLine.LF).textBuffer; const sizeBufferLimitPosition = buffer.getPositionAt(SIZE_LIMIT); if (sizeBufferLimitPosition.lineNumber < LINES_LIMIT) { const truncatedText = buffer.getValueInRange(new Range(1, 1, sizeBufferLimitPosition.lineNumber, sizeBufferLimitPosition.column), EndOfLinePreference.TextDefined); if (renderANSI) { container.appendChild(handleANSIOutput(truncatedText, themeService)); } else { - const pre = DOM.$('div'); + const pre = DOM.$('pre'); pre.innerText = truncatedText; container.appendChild(pre); } @@ -83,21 +83,32 @@ export function truncatedArrayOfString(container: HTMLElement, outputs: string[] const bufferBuilder = new PieceTreeTextBufferBuilder(); outputs.forEach(output => bufferBuilder.acceptChunk(output)); const factory = bufferBuilder.finish(); - buffer = factory.create(DefaultEndOfLine.LF); + buffer = factory.create(DefaultEndOfLine.LF).textBuffer; } if (buffer.getLineCount() < LINES_LIMIT) { const lineCount = buffer.getLineCount(); - const fullRange = new Range(1, 1, lineCount, buffer.getLineLastNonWhitespaceColumn(lineCount)); + const fullRange = new Range(1, 1, lineCount, Math.max(1, buffer.getLineLastNonWhitespaceColumn(lineCount))); - container.innerText = buffer.getValueInRange(fullRange, EndOfLinePreference.TextDefined); + if (renderANSI) { + const pre = DOM.$('pre'); + container.appendChild(pre); + + pre.appendChild(handleANSIOutput(buffer.getValueInRange(fullRange, EndOfLinePreference.TextDefined), themeService)); + } else { + const pre = DOM.$('pre'); + container.appendChild(pre); + pre.innerText = buffer.getValueInRange(fullRange, EndOfLinePreference.TextDefined); + } return; } if (renderANSI) { - container.appendChild(handleANSIOutput(buffer.getValueInRange(new Range(1, 1, LINES_LIMIT - 5, buffer.getLineLastNonWhitespaceColumn(LINES_LIMIT - 5)), EndOfLinePreference.TextDefined), themeService)); + const pre = DOM.$('pre'); + container.appendChild(pre); + pre.appendChild(handleANSIOutput(buffer.getValueInRange(new Range(1, 1, LINES_LIMIT - 5, buffer.getLineLastNonWhitespaceColumn(LINES_LIMIT - 5)), EndOfLinePreference.TextDefined), themeService)); } else { - const pre = DOM.$('div'); + const pre = DOM.$('pre'); pre.innerText = buffer.getValueInRange(new Range(1, 1, LINES_LIMIT - 5, buffer.getLineLastNonWhitespaceColumn(LINES_LIMIT - 5)), EndOfLinePreference.TextDefined); container.appendChild(pre); } @@ -108,7 +119,9 @@ export function truncatedArrayOfString(container: HTMLElement, outputs: string[] const lineCount = buffer.getLineCount(); if (renderANSI) { - container.appendChild(handleANSIOutput(buffer.getValueInRange(new Range(lineCount - 5, 1, lineCount, buffer.getLineLastNonWhitespaceColumn(lineCount)), EndOfLinePreference.TextDefined), themeService)); + const pre = DOM.$('div'); + container.appendChild(pre); + pre.appendChild(handleANSIOutput(buffer.getValueInRange(new Range(lineCount - 5, 1, lineCount, buffer.getLineLastNonWhitespaceColumn(lineCount)), EndOfLinePreference.TextDefined), themeService)); } else { const post = DOM.$('div'); post.innerText = buffer.getValueInRange(new Range(lineCount - 5, 1, lineCount, buffer.getLineLastNonWhitespaceColumn(lineCount)), EndOfLinePreference.TextDefined); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 31fc6590e..ce56dffa3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -10,14 +10,11 @@ import { isWeb } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; -import { CELL_MARGIN, CELL_RUN_GUTTER, CODE_CELL_LEFT_MARGIN, CELL_OUTPUT_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; -import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { CellOutputKind, IDisplayOutput, IInsetRenderOutput, INotebookRendererInfo, IProcessedOutput, ITransformedDisplayOutputDto, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICommonCellInfo, ICommonNotebookEditor, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellOutputKind, IDisplayOutput, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewService, WebviewElement, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview'; import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { dirname, joinPath } from 'vs/base/common/resources'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { preloadsScriptStr } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; @@ -26,6 +23,7 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IFileService } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; import { getExtensionForMimeType } from 'vs/base/common/mime'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export interface WebviewIntialized { __vscode_notebook_message: boolean; @@ -36,6 +34,7 @@ export interface IDimensionMessage { __vscode_notebook_message: boolean; type: 'dimension'; id: string; + init: boolean; data: DOM.Dimension; } @@ -196,9 +195,9 @@ export type ToWebviewMessage = export type AnyMessage = FromWebviewMessage | ToWebviewMessage; -interface ICachedInset { +export interface ICachedInset { outputId: string; - cell: CodeCellViewModel; + cellInfo: K; renderer?: INotebookRendererInfo; cachedCreation: ICreationRequestMessage; } @@ -217,12 +216,12 @@ export interface INotebookWebviewMessage { } let version = 0; -export class BackLayerWebView extends Disposable { +export class BackLayerWebView extends Disposable { element: HTMLElement; webview: WebviewElement | undefined = undefined; - insetMapping: Map = new Map(); - hiddenInsetMapping: Set = new Set(); - reversedInsetMapping: Map = new Map(); + insetMapping: Map> = new Map(); + hiddenInsetMapping: Set = new Set(); + reversedInsetMapping: Map = new Map(); localResourceRootsCache: URI[] | undefined = undefined; rendererRootsCache: URI[] = []; kernelRootsCache: URI[] = []; @@ -234,9 +233,13 @@ export class BackLayerWebView extends Disposable { private _disposed = false; constructor( - public notebookEditor: INotebookEditor, + public notebookEditor: ICommonNotebookEditor, public id: string, public documentUri: URI, + public options: { + outputNodePadding: number, + outputNodeLeftPadding: number + }, @IWebviewService readonly webviewService: IWebviewService, @IOpenerService readonly openerService: IOpenerService, @INotebookService private readonly notebookService: INotebookService, @@ -249,12 +252,10 @@ export class BackLayerWebView extends Disposable { this.element = document.createElement('div'); - this.element.style.width = `calc(100% - ${CODE_CELL_LEFT_MARGIN + (CELL_MARGIN * 2) + CELL_RUN_GUTTER}px)`; this.element.style.height = '1400px'; this.element.style.position = 'absolute'; - this.element.style.margin = `0px 0 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px`; } - generateContent(outputNodePadding: number, coreDependencies: string, baseUrl: string) { + generateContent(coreDependencies: string, baseUrl: string) { return html` @@ -263,7 +264,7 @@ export class BackLayerWebView extends Disposable {