diff --git a/.nvmrc b/.nvmrc index 32c861f970d..45a4fb75db8 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -8.9.2 +8 diff --git a/.vscode/launch.json b/.vscode/launch.json index e217c585b42..0cde845dc24 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -123,7 +123,8 @@ "request": "launch", "name": "Launch VS Code", "windows": { - "runtimeExecutable": "${workspaceFolder}/scripts/code.bat" + "runtimeExecutable": "${workspaceFolder}/scripts/code.bat", + "timeout": 20000 }, "osx": { "runtimeExecutable": "${workspaceFolder}/scripts/code.sh" diff --git a/.yarnrc b/.yarnrc index 9e184a6f5ac..190252d15da 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://atom.io/download/electron" -target "3.1.0" +target "3.1.2" runtime "electron" diff --git a/README.md b/README.md index d5bebc76f27..b08b3fc38f0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Visual Studio Code - Open Source -[![Build Status](https://vscode.visualstudio.com/_apis/public/build/definitions/a4cdce18-a05c-4bb8-9476-5d07e63bfd76/1/badge?branchName=master)](https://aka.ms/vscode-builds) +[![Build Status](https://dev.azure.com/vscode/VSCode/_apis/build/status/VS%20Code?branchName=master)](https://aka.ms/vscode-builds) [![Feature Requests](https://img.shields.io/github/issues/Microsoft/vscode/feature-request.svg)](https://github.com/Microsoft/vscode/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request+sort%3Areactions-%2B1-desc) [![Bugs](https://img.shields.io/github/issues/Microsoft/vscode/bug.svg)](https://github.com/Microsoft/vscode/issues?utf8=✓&q=is%3Aissue+is%3Aopen+label%3Abug) [![Gitter](https://img.shields.io/badge/chat-on%20gitter-yellow.svg)](https://gitter.im/Microsoft/vscode) diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 4541dbbb24d..91876a89245 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -5,47 +5,47 @@ Do Not Translate or Localize This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Microsoft received such components are set forth below. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. -1. atom/language-c (https://github.com/atom/language-c) -2. atom/language-clojure (https://github.com/atom/language-clojure) -3. atom/language-coffee-script (https://github.com/atom/language-coffee-script) +1. atom/language-c version 0.58.1 (https://github.com/atom/language-c) +2. atom/language-clojure version 0.22.6 (https://github.com/atom/language-clojure) +3. atom/language-coffee-script version 0.49.3 (https://github.com/atom/language-coffee-script) 4. atom/language-css (https://github.com/atom/language-css) -5. atom/language-java (https://github.com/atom/language-java) -6. atom/language-objective-c (https://github.com/atom/language-objective-c) -7. atom/language-sass version 0.52.0 (https://github.com/atom/language-sass) -8. atom/language-shellscript (https://github.com/atom/language-shellscript) -9. atom/language-xml (https://github.com/atom/language-xml) +5. atom/language-java version 0.31.1 (https://github.com/atom/language-java) +6. atom/language-objective-c version 0.15.0 (https://github.com/atom/language-objective-c) +7. atom/language-sass version 0.61.4 (https://github.com/atom/language-sass) +8. atom/language-shellscript version 0.26.0 (https://github.com/atom/language-shellscript) +9. atom/language-xml version 0.35.2 (https://github.com/atom/language-xml) 10. chriskempson/tomorrow-theme (https://github.com/chriskempson/tomorrow-theme) 11. Colorsublime-Themes version 0.1.0 (https://github.com/Colorsublime/Colorsublime-Themes) -12. daaain/Handlebars (https://github.com/daaain/Handlebars) +12. daaain/Handlebars version 1.7.1 (https://github.com/daaain/Handlebars) 13. davidrios/pug-tmbundle (https://github.com/davidrios/pug-tmbundle) 14. definitelytyped (https://github.com/DefinitelyTyped/DefinitelyTyped) -15. demyte/language-cshtml (https://github.com/demyte/language-cshtml) +15. demyte/language-cshtml version 0.3.0 (https://github.com/demyte/language-cshtml) 16. Document Object Model version 4.0.0 (https://www.w3.org/DOM/) 17. dotnet/csharp-tmLanguage version 0.1.0 (https://github.com/dotnet/csharp-tmLanguage) 18. expand-abbreviation version 0.5.8 (https://github.com/emmetio/expand-abbreviation) 19. fadeevab/make.tmbundle (https://github.com/fadeevab/make.tmbundle) 20. freebroccolo/atom-language-swift (https://github.com/freebroccolo/atom-language-swift) 21. HTML 5.1 W3C Working Draft version 08 October 2015 (http://www.w3.org/TR/2015/WD-html51-20151008/) -22. Ikuyadeu/vscode-R (https://github.com/Ikuyadeu/vscode-R) +22. Ikuyadeu/vscode-R version 0.5.5 (https://github.com/Ikuyadeu/vscode-R) 23. Ionic documentation version 1.2.4 (https://github.com/ionic-team/ionic-site) 24. ionide/ionide-fsgrammar (https://github.com/ionide/ionide-fsgrammar) 25. js-beautify version 1.6.8 (https://github.com/beautify-web/js-beautify) 26. Jxck/assert version 1.0.0 (https://github.com/Jxck/assert) 27. language-docker (https://github.com/moby/moby) -28. language-go version 0.39.0 (https://github.com/atom/language-go) -29. language-less (https://github.com/atom/language-less) -30. language-php (https://github.com/atom/language-php) -31. language-rust version 0.4.9 (https://github.com/zargony/atom-language-rust) -32. MagicStack/MagicPython (https://github.com/MagicStack/MagicPython) +28. language-go version 0.44.3 (https://github.com/atom/language-go) +29. language-less version 0.34.2 (https://github.com/atom/language-less) +30. language-php version 0.43.2 (https://github.com/atom/language-php) +31. language-rust version 0.4.12 (https://github.com/zargony/atom-language-rust) +32. MagicStack/MagicPython version 1.1.1 (https://github.com/MagicStack/MagicPython) 33. marked version 0.5.0 (https://github.com/markedjs/marked) 34. mdn-data version 1.1.12 (https://github.com/mdn/data) 35. Microsoft/TypeScript-TmLanguage version 0.0.1 (https://github.com/Microsoft/TypeScript-TmLanguage) 36. Microsoft/vscode-JSON.tmLanguage (https://github.com/Microsoft/vscode-JSON.tmLanguage) -37. Microsoft/vscode-mssql (https://github.com/Microsoft/vscode-mssql) -38. mmims/language-batchfile (https://github.com/mmims/language-batchfile) -39. octicons-code version 3.1.0 (https://registry.npmjs.org/octicons/-/octicons-3.1.0.tgz) -40. octicons-font version 3.1.0 (https://registry.npmjs.org/octicons/-/octicons-3.1.0.tgz) -41. PowerShell/EditorSyntax (https://github.com/powershell/editorsyntax) +37. Microsoft/vscode-mssql version 1.4.0 (https://github.com/Microsoft/vscode-mssql) +38. mmims/language-batchfile version 0.7.4 (https://github.com/mmims/language-batchfile) +39. octicons version 8.3.0 (https://github.com/primer/octicons) +40. PowerShell/EditorSyntax (https://github.com/powershell/editorsyntax) +41. promise-polyfill version 8.0.0 (https://github.com/taylorhakes/promise-polyfill) 42. seti-ui version 0.1.0 (https://github.com/jesseweed/seti-ui) 43. shaders-tmLanguage version 0.1.0 (https://github.com/tgjones/shaders-tmLanguage) 44. textmate/asp.vb.net.tmbundle (https://github.com/textmate/asp.vb.net.tmbundle) @@ -62,10 +62,12 @@ This project incorporates components from the projects listed below. The origina 55. textmate/ruby.tmbundle (https://github.com/textmate/ruby.tmbundle) 56. textmate/yaml.tmbundle (https://github.com/textmate/yaml.tmbundle) 57. TypeScript-TmLanguage version 0.1.8 (https://github.com/Microsoft/TypeScript-TmLanguage) -58. Unicode version 12.0.0 (http://www.unicode.org/) -59. vscode-logfile-highlighter version 1.2.0 (https://github.com/emilast/vscode-logfile-highlighter) -60. vscode-swift version 0.0.1 (https://github.com/owensd/vscode-swift) -61. Web Background Synchronization (https://github.com/WICG/BackgroundSync) +58. TypeScript-TmLanguage version 1.0.0 (https://github.com/Microsoft/TypeScript-TmLanguage) +59. Unicode version 12.0.0 (http://www.unicode.org/) +60. vscode-logfile-highlighter version 2.4.1 (https://github.com/emilast/vscode-logfile-highlighter) +61. vscode-octicons-font version 1.0.0 (https://github.com/Microsoft/vscode-octicons-font) +62. vscode-swift version 0.0.1 (https://github.com/owensd/vscode-swift) +63. Web Background Synchronization (https://github.com/WICG/BackgroundSync) %% atom/language-c NOTICES AND INFORMATION BEGIN HERE @@ -1800,11 +1802,11 @@ THE SOFTWARE. ========================================= END OF mmims/language-batchfile NOTICES AND INFORMATION -%% octicons-code NOTICES AND INFORMATION BEGIN HERE +%% octicons NOTICES AND INFORMATION BEGIN HERE ========================================= -The MIT License (MIT) +MIT License -(c) 2012-2015 GitHub +Copyright (c) 2019 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -1813,109 +1815,18 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ========================================= -END OF octicons-code NOTICES AND INFORMATION - -%% octicons-font NOTICES AND INFORMATION BEGIN HERE -========================================= -(c) 2012-2015 GitHub - -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. -========================================= -END OF octicons-font NOTICES AND INFORMATION +END OF octicons NOTICES AND INFORMATION %% PowerShell/EditorSyntax NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -1945,6 +1856,33 @@ SOFTWARE. ========================================= END OF PowerShell/EditorSyntax NOTICES AND INFORMATION +%% promise-polyfill NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Taylor Hakes +Copyright (c) 2014 Forbes Lindesay + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF promise-polyfill NOTICES AND INFORMATION + %% seti-ui NOTICES AND INFORMATION BEGIN HERE ========================================= Copyright (c) 2014 Jesse Weed @@ -2357,6 +2295,32 @@ SOFTWARE. ========================================= END OF vscode-logfile-highlighter NOTICES AND INFORMATION +%% vscode-octicons-font NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE +========================================= +END OF vscode-octicons-font NOTICES AND INFORMATION + %% vscode-swift NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index 5bc85092c84..1ce94db95eb 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -1,7 +1,7 @@ [ { "name": "ms-vscode.node-debug", - "version": "1.31.1", + "version": "1.31.2", "repo": "https://github.com/Microsoft/vscode-node-debug", "metadata": { "id": "b6ded8fb-a0a0-4c1c-acbd-ab2a3bc995a6", @@ -16,7 +16,7 @@ }, { "name": "ms-vscode.node-debug2", - "version": "1.31.3", + "version": "1.31.5", "repo": "https://github.com/Microsoft/vscode-node-debug2", "metadata": { "id": "36d19e17-7569-4841-a001-947eb18602b2", @@ -31,7 +31,7 @@ }, { "name": "ms-vscode.references-view", - "version": "0.0.23", + "version": "0.0.24", "repo": "https://github.com/Microsoft/vscode-reference-view", "metadata": { "id": "dc489f46-520d-4556-ae85-1f9eab3c412d", diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 006235ebeaf..bddba8fe524 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -215,13 +215,10 @@ function prepareSnapPackage(arch) { .pipe(replace('@@VERSION@@', commit.substr(0, 8))) .pipe(rename('snap/snapcraft.yaml')); - const snapUpdate = gulp.src('resources/linux/snap/snapUpdate.sh', { base: '.' }) - .pipe(rename(`usr/share/${product.applicationName}/snapUpdate.sh`)); - const electronLaunch = gulp.src('resources/linux/snap/electron-launch', { base: '.' }) .pipe(rename('electron-launch')); - const all = es.merge(desktop, icon, code, snapcraft, electronLaunch, snapUpdate); + const all = es.merge(desktop, icon, code, snapcraft, electronLaunch); return all.pipe(vfs.dest(destination)); }; diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index ea2d2d9a2dc..34a8e28ff1a 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -17,6 +17,7 @@ function yarnInstall(location, opts) { opts.cwd = location; opts.stdio = 'inherit'; + console.log('Installing dependencies in \'%s\'.', location); const result = cp.spawnSync(yarn, ['install'], opts); if (result.error || result.status !== 0) { diff --git a/cgmanifest.json b/cgmanifest.json index 89693a02d32..b7e74e39456 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -48,7 +48,7 @@ "git": { "name": "libchromiumcontent", "repositoryUrl": "https://github.com/electron/libchromiumcontent", - "commitHash": "29e02cd4c37777734f97d00b5a538d7c7acfa67a" + "commitHash": "7ea271f92018b1eeb8e70ec6de8c29f9758a0c05" } }, "isOnlyProductionDependency": true, @@ -73,12 +73,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "4913fc81d1dab21f4b15c4cb682a218072447fed" + "commitHash": "bb28fa8e8e797db249a66405146ad0501eaf411a" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "3.1.0" + "version": "3.1.2" }, { "component": { @@ -111,12 +111,24 @@ "git": { "name": "vscode-octicons-font", "repositoryUrl": "https://github.com/Microsoft/vscode-octicons-font", - "commitHash": "174697a8d28a65dc6600c44d239bd75f7d157c71" + "commitHash": "5095860bb929919670646e2dfa0ee47d9b93bcb9" } }, "license": "MIT", "version": "1.0.0" }, + { + "component": { + "type": "git", + "git": { + "name": "octicons", + "repositoryUrl": "https://github.com/primer/octicons", + "commitHash": "d120bf97bc9a12fb415f69fedaf31fe58427ca56" + } + }, + "license": "MIT", + "version": "8.3.0" + }, { "component": { "type": "npm", @@ -505,6 +517,19 @@ " defined by the Mozilla Public License, v. 2.0." ], "license": "MPL" + }, + { + "component": { + "type": "git", + "git": { + "name": "ripgrep", + "repositoryUrl": "https://github.com/BurntSushi/ripgrep", + "commitHash": "8a7db1a918e969b85cd933d8ed9fa5285b281ba4" + } + }, + "isOnlyProductionDependency": true, + "license": "MIT", + "version": "0.10.0" } ], "version": 1 diff --git a/extensions/coffeescript/cgmanifest.json b/extensions/coffeescript/cgmanifest.json index 3731fb9e3ff..3c792173e6e 100644 --- a/extensions/coffeescript/cgmanifest.json +++ b/extensions/coffeescript/cgmanifest.json @@ -6,12 +6,12 @@ "git": { "name": "atom/language-coffee-script", "repositoryUrl": "https://github.com/atom/language-coffee-script", - "commitHash": "a0da2a73ad817e2fc13c2ef8fcd2624017c39610" + "commitHash": "0f6db9143663e18b1ad00667820f46747dba495e" } }, "license": "MIT", "description": "The file syntaxes/coffeescript.tmLanguage.json was derived from the Atom package https://github.com/atom/language-coffee-script which was originally converted from the TextMate bundle https://github.com/jashkenas/coffee-script-tmbundle.", - "version": "0.49.2" + "version": "0.49.3" } ], "version": 1 diff --git a/extensions/coffeescript/syntaxes/coffeescript.tmLanguage.json b/extensions/coffeescript/syntaxes/coffeescript.tmLanguage.json index 5fcff295c61..708856898bc 100644 --- a/extensions/coffeescript/syntaxes/coffeescript.tmLanguage.json +++ b/extensions/coffeescript/syntaxes/coffeescript.tmLanguage.json @@ -4,10 +4,13 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/atom/language-coffee-script/commit/a0da2a73ad817e2fc13c2ef8fcd2624017c39610", + "version": "https://github.com/atom/language-coffee-script/commit/0f6db9143663e18b1ad00667820f46747dba495e", "name": "CoffeeScript", "scopeName": "source.coffee", "patterns": [ + { + "include": "#jsx" + }, { "match": "(new)\\s+(?:(?:(class)\\s+(\\w+(?:\\.\\w*)*)?)|(\\w+(?:\\.\\w*)*))", "name": "meta.class.instance.constructor.coffee", @@ -1213,6 +1216,101 @@ "include": "#embedded_comment" } ] + }, + "jsx": { + "patterns": [ + { + "include": "#jsx-tag" + }, + { + "include": "#jsx-end-tag" + } + ] + }, + "jsx-expression": { + "begin": "{", + "beginCaptures": { + "0": { + "name": "meta.brace.curly.coffee" + } + }, + "end": "}", + "endCaptures": { + "0": { + "name": "meta.brace.curly.coffee" + } + }, + "patterns": [ + { + "include": "#double_quoted_string" + }, + { + "include": "$self" + } + ] + }, + "jsx-attribute": { + "patterns": [ + { + "captures": { + "1": { + "name": "entity.other.attribute-name.coffee" + }, + "2": { + "name": "keyword.operator.assignment.coffee" + } + }, + "match": "(?:^|\\s+)([-\\w.]+)\\s*(=)" + }, + { + "include": "#double_quoted_string" + }, + { + "include": "#single_quoted_string" + }, + { + "include": "#jsx-expression" + } + ] + }, + "jsx-tag": { + "patterns": [ + { + "begin": "(<)([-\\w\\.]+)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.tag.coffee" + }, + "2": { + "name": "entity.name.tag.coffee" + } + }, + "end": "(/?>)", + "name": "meta.tag.coffee", + "patterns": [ + { + "include": "#jsx-attribute" + } + ] + } + ] + }, + "jsx-end-tag": { + "patterns": [ + { + "begin": "()", + "name": "meta.tag.coffee" + } + ] } } } \ No newline at end of file diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index e6b137809ff..d4ac414897d 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -44,8 +44,11 @@ export class SettingsDocument { const completions: vscode.CompletionItem[] = []; completions.push(this.newSimpleCompletionItem('${activeEditorShort}', range, localize('activeEditorShort', "the file name (e.g. myFile.txt)"))); - completions.push(this.newSimpleCompletionItem('${activeEditorMedium}', range, localize('activeEditorMedium', "the path of the file relative to the workspace folder (e.g. myFolder/myFile.txt)"))); - completions.push(this.newSimpleCompletionItem('${activeEditorLong}', range, localize('activeEditorLong', "the full path of the file (e.g. /Users/Development/myProject/myFolder/myFile.txt)"))); + completions.push(this.newSimpleCompletionItem('${activeEditorMedium}', range, localize('activeEditorMedium', "the path of the file relative to the workspace folder (e.g. myFolder/myFileFolder/myFile.txt)"))); + completions.push(this.newSimpleCompletionItem('${activeEditorLong}', range, localize('activeEditorLong', "the full path of the file (e.g. /Users/Development/myFolder/myFileFolder/myFile.txt)"))); + completions.push(this.newSimpleCompletionItem('${activeFolderShort}', range, localize('activeFolderShort', "the name of the folder the file is contained in (e.g. myFileFolder)"))); + completions.push(this.newSimpleCompletionItem('${activeFolderMedium}', range, localize('activeFolderMedium', "the path of the folder the file is contained in, relative to the workspace folder (e.g. myFolder/myFileFolder)"))); + completions.push(this.newSimpleCompletionItem('${activeFolderLong}', range, localize('activeFolderLong', "the full path of the folder the file is contained in (e.g. /Users/Development/myFolder/myFileFolder)"))); completions.push(this.newSimpleCompletionItem('${rootName}', range, localize('rootName', "name of the workspace (e.g. myFolder or myWorkspace)"))); completions.push(this.newSimpleCompletionItem('${rootPath}', range, localize('rootPath', "file path of the workspace (e.g. /Users/Development/myWorkspace)"))); completions.push(this.newSimpleCompletionItem('${folderName}', range, localize('folderName', "name of the workspace folder the file is contained in (e.g. myFolder)"))); diff --git a/extensions/css-language-features/.vscode/launch.json b/extensions/css-language-features/.vscode/launch.json index d6393141c5d..4f7166e6dce 100644 --- a/extensions/css-language-features/.vscode/launch.json +++ b/extensions/css-language-features/.vscode/launch.json @@ -23,8 +23,7 @@ "outFiles": [ "${workspaceFolder}/client/out/**/*.js" ], - "smartStep": true, - "preLaunchTask": "npm: compile" + "smartStep": true }, { "name": "Launch Tests", @@ -39,8 +38,7 @@ "sourceMaps": true, "outFiles": [ "${workspaceFolder}/client/out/test/**/*.js" - ], - "preLaunchTask": "npm: compile" + ] }, { "name": "Attach Language Server", diff --git a/extensions/css-language-features/.vscode/settings.json b/extensions/css-language-features/.vscode/settings.json new file mode 100644 index 00000000000..5b30e2983c2 --- /dev/null +++ b/extensions/css-language-features/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "prettier.semi": true, + "prettier.singleQuote": true, + "prettier.printWidth": 120, +} \ No newline at end of file diff --git a/extensions/css-language-features/client/src/cssMain.ts b/extensions/css-language-features/client/src/cssMain.ts index ee8c9973d3a..59bd8db7d26 100644 --- a/extensions/css-language-features/client/src/cssMain.ts +++ b/extensions/css-language-features/client/src/cssMain.ts @@ -9,8 +9,8 @@ import * as fs from 'fs'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -import { languages, window, commands, ExtensionContext, Range, Position, CompletionItem, CompletionItemKind, TextEdit, SnippetString, workspace } from 'vscode'; -import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, Disposable } from 'vscode-languageclient'; +import { languages, window, commands, ExtensionContext, Range, Position, CompletionItem, CompletionItemKind, TextEdit, SnippetString, workspace, TextDocument, SelectionRange, SelectionRangeKind } from 'vscode'; +import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, Disposable, TextDocumentIdentifier } from 'vscode-languageclient'; import { getCustomDataPathsInAllWorkspaces, getCustomDataPathsFromAllExtensions } from './customData'; // this method is called when vs code is activated @@ -78,6 +78,42 @@ export function activate(context: ExtensionContext) { client.onReady().then(() => { context.subscriptions.push(initCompletionProvider()); + + documentSelector.forEach(selector => { + context.subscriptions.push(languages.registerSelectionRangeProvider(selector, { + async provideSelectionRanges(document: TextDocument, position: Position): Promise { + const textDocument = client.code2ProtocolConverter.asTextDocumentIdentifier(document); + const rawRanges = await client.sendRequest('$/textDocument/selectionRange', { textDocument, position }); + if (Array.isArray(rawRanges)) { + return rawRanges.map(r => { + return { + range: client.protocol2CodeConverter.asRange(r), + kind: SelectionRangeKind.Declaration + }; + }); + } + return []; + } + })); + }); + }); + + const selectionRangeProvider = { + async provideSelectionRanges(document: TextDocument, position: Position): Promise { + const textDocument = TextDocumentIdentifier.create(document.uri.toString()); + const rawRanges: Range[] = await client.sendRequest('$/textDocument/selectionRange', { textDocument, position }); + + return rawRanges.map(r => { + const actualRange = new Range(new Position(r.start.line, r.start.character), new Position(r.end.line, r.end.character)); + return { + range: actualRange, + kind: SelectionRangeKind.Declaration + }; + }); + } + }; + documentSelector.forEach(selector => { + languages.registerSelectionRangeProvider(selector, selectionRangeProvider); }); function initCompletionProvider(): Disposable { diff --git a/extensions/css-language-features/client/src/customData.ts b/extensions/css-language-features/client/src/customData.ts index c9954de8c51..8ba0770235a 100644 --- a/extensions/css-language-features/client/src/customData.ts +++ b/extensions/css-language-features/client/src/customData.ts @@ -43,7 +43,7 @@ export function getCustomDataPathsFromAllExtensions(): string[] { for (const extension of extensions.all) { const contributes = extension.packageJSON && extension.packageJSON.contributes; - if (contributes && contributes.css && contributes.css.customData && Array.isArray(contributes.css.customData)) { + if (contributes && contributes.css && contributes.css.experimental.customData && Array.isArray(contributes.css.experimental.customData)) { const relativePaths: string[] = contributes.css.customData; relativePaths.forEach(rp => { dataPaths.push(path.resolve(extension.extensionPath, rp)); diff --git a/extensions/css-language-features/client/src/typings/ref.d.ts b/extensions/css-language-features/client/src/typings/ref.d.ts index 9c1a5df18ed..de602d3f8d6 100644 --- a/extensions/css-language-features/client/src/typings/ref.d.ts +++ b/extensions/css-language-features/client/src/typings/ref.d.ts @@ -2,5 +2,5 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - /// +/// diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index abb927c319c..514196d3608 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -15,6 +15,7 @@ "onCommand:_css.applyCodeAction" ], "main": "./client/out/cssMain", + "enableProposedApi": true, "scripts": { "compile": "gulp compile-extension:css-language-features-client compile-extension:css-language-features-server", "watch": "gulp watch-extension:css-language-features-client watch-extension:css-language-features-server", diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index 3e87558befb..aebf551a2e5 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -9,7 +9,7 @@ }, "main": "./out/cssServerMain", "dependencies": { - "vscode-css-languageservice": "^3.0.13-next.6", + "vscode-css-languageservice": "^3.0.13-next.10", "vscode-languageserver": "^5.1.0" }, "devDependencies": { diff --git a/extensions/css-language-features/server/src/cssServerMain.ts b/extensions/css-language-features/server/src/cssServerMain.ts index 0108e1df628..3ef166f1b80 100644 --- a/extensions/css-language-features/server/src/cssServerMain.ts +++ b/extensions/css-language-features/server/src/cssServerMain.ts @@ -7,15 +7,14 @@ import { createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities, ConfigurationRequest, WorkspaceFolder } from 'vscode-languageserver'; import URI from 'vscode-uri'; -import * as fs from 'fs'; -import { TextDocument, CompletionList } from 'vscode-languageserver-types'; +import { TextDocument, CompletionList, Position } from 'vscode-languageserver-types'; -import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet, CSSData } from 'vscode-css-languageservice'; +import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet } from 'vscode-css-languageservice'; import { getLanguageModelCache } from './languageModelCache'; import { getPathCompletionParticipant } from './pathCompletion'; import { formatError, runSafe } from './utils/runner'; import { getDocumentContext } from './utils/documentContext'; -import { parseCSSData } from './utils/languageFacts'; +import { getDataProviders } from './customData'; export interface Settings { css: LanguageSettings; @@ -66,17 +65,7 @@ connection.onInitialize((params: InitializeParams): InitializeResult => { } const dataPaths: string[] = params.initializationOptions.dataPaths; - - const customDataCollections: CSSData[] = []; - - dataPaths.forEach(p => { - if (fs.existsSync(p)) { - const data = parseCSSData(fs.readFileSync(p, 'utf-8')); - customDataCollections.push(data); - } else { - return; - } - }); + const customDataProviders = getDataProviders(dataPaths); function getClientCapability(name: string, def: T) { const keys = name.split('.'); @@ -93,9 +82,9 @@ connection.onInitialize((params: InitializeParams): InitializeResult => { scopedSettingsSupport = !!getClientCapability('workspace.configuration', false); foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); - languageServices.css = getCSSLanguageService({ customDataCollections }); - languageServices.scss = getSCSSLanguageService({ customDataCollections }); - languageServices.less = getLESSLanguageService({ customDataCollections }); + languageServices.css = getCSSLanguageService({ customDataProviders }); + languageServices.scss = getSCSSLanguageService({ customDataProviders }); + languageServices.less = getLESSLanguageService({ customDataProviders }); const capabilities: ServerCapabilities = { // Tell the client that the server works in FULL text document sync mode @@ -346,5 +335,19 @@ connection.onFoldingRanges((params, token) => { }, null, `Error while computing folding ranges for ${params.textDocument.uri}`, token); }); +connection.onRequest('$/textDocument/selectionRange', async (params, token) => { + return runSafe(() => { + const document = documents.get(params.textDocument.uri); + const position: Position = params.position; + + if (document) { + const stylesheet = stylesheets.get(document); + return getLanguageService(document).getSelectionRanges(document, position, stylesheet); + } + return Promise.resolve(null); + }, null, `Error while computing selection ranges for ${params.textDocument.uri}`, token); +}); + + // Listen on the connection connection.listen(); \ No newline at end of file diff --git a/extensions/css-language-features/server/src/customData.ts b/extensions/css-language-features/server/src/customData.ts new file mode 100644 index 00000000000..b5cbdc27c7a --- /dev/null +++ b/extensions/css-language-features/server/src/customData.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CSSDataV1, ICSSDataProvider } from 'vscode-css-languageservice'; +import * as fs from 'fs'; + +export function getDataProviders(dataPaths: string[]): ICSSDataProvider[] { + const providers = dataPaths.map(p => { + if (fs.existsSync(p)) { + const data = parseCSSData(fs.readFileSync(p, 'utf-8')); + return { + provideProperties: () => data.properties || [], + provideAtDirectives: () => data.atDirectives || [], + providePseudoClasses: () => data.pseudoClasses || [], + providePseudoElements: () => data.pseudoElements || [] + }; + } else { + return { + provideProperties: () => [], + provideAtDirectives: () => [], + providePseudoClasses: () => [], + providePseudoElements: () => [] + }; + } + }); + + return providers; +} + +function parseCSSData(source: string): CSSDataV1 { + let rawData: any; + + try { + rawData = JSON.parse(source); + } catch (err) { + return { + version: 1 + }; + } + + return { + version: 1, + properties: rawData.properties || [], + atDirectives: rawData.atdirectives || [], + pseudoClasses: rawData.pseudoclasses || [], + pseudoElements: rawData.pseudoelements || [] + }; +} diff --git a/extensions/css-language-features/server/src/utils/languageFacts.ts b/extensions/css-language-features/server/src/utils/languageFacts.ts deleted file mode 100644 index c6d9b2ee641..00000000000 --- a/extensions/css-language-features/server/src/utils/languageFacts.ts +++ /dev/null @@ -1,23 +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 { CSSData } from 'vscode-css-languageservice'; - -export function parseCSSData(source: string): CSSData { - let rawData: any; - - try { - rawData = JSON.parse(source); - } catch (err) { - return {}; - } - - return { - properties: rawData.properties || [], - atDirectives: rawData.atdirectives || [], - pseudoClasses: rawData.pseudoclasses || [], - pseudoElements: rawData.pseudoelements || [] - }; -} diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index 813bbb333c6..665dd086b76 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -229,10 +229,10 @@ supports-color@5.4.0: dependencies: has-flag "^3.0.0" -vscode-css-languageservice@^3.0.13-next.6: - version "3.0.13-next.6" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-3.0.13-next.6.tgz#0329ea1da29ca84d1821cd32ee10bca0ee4c50cd" - integrity sha512-wC8zaFWHNnqIaOT4LXByy3NyTl916uHxGy3U3cpV7Gw7F8ENylSGM2RAO+l7NohIbP0WZlet441HEwSbb+bZbQ== +vscode-css-languageservice@^3.0.13-next.10: + version "3.0.13-next.10" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-3.0.13-next.10.tgz#f5822e832b06e1e91ec96c528bab83bb07251c35" + integrity sha512-zKwzo3GVhrAllYDM4afL8q1XCHixsI8tP3SyLrWGzp0Nc9P+bbjKQeC26VcaOb0dtkgfpB/vfBPf+4yOs4s/pw== dependencies: vscode-languageserver-types "^3.13.0" vscode-nls "^4.0.0" diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index b4dba92db69..25742babc15 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -5,7 +5,7 @@ import { Model } from '../model'; import { Repository as BaseRepository, Resource } from '../repository'; -import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status } from './git'; +import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions } from './git'; import { Event, SourceControlInputBox, Uri, SourceControl } from 'vscode'; import { mapEvent } from '../util'; @@ -76,6 +76,10 @@ export class ApiRepository implements Repository { return this._repository.setConfig(key, value); } + getGlobalConfig(key: string): Promise { + return this._repository.getGlobalConfig(key); + } + getObjectDetails(treeish: string, path: string): Promise<{ mode: string; object: string; size: number; }> { return this._repository.getObjectDetails(treeish, path); } @@ -104,19 +108,27 @@ export class ApiRepository implements Repository { return this._repository.diff(cached); } - diffWithHEAD(path: string): Promise { + diffWithHEAD(): Promise; + diffWithHEAD(path: string): Promise; + diffWithHEAD(path?: string): Promise { return this._repository.diffWithHEAD(path); } - diffWith(ref: string, path: string): Promise { + diffWith(ref: string): Promise; + diffWith(ref: string, path: string): Promise; + diffWith(ref: string, path?: string): Promise { return this._repository.diffWith(ref, path); } - diffIndexWithHEAD(path: string): Promise { + diffIndexWithHEAD(): Promise; + diffIndexWithHEAD(path: string): Promise; + diffIndexWithHEAD(path?: string): Promise { return this._repository.diffIndexWithHEAD(path); } - diffIndexWith(ref: string, path: string): Promise { + diffIndexWith(ref: string): Promise; + diffIndexWith(ref: string, path: string): Promise; + diffIndexWith(ref: string, path?: string): Promise { return this._repository.diffIndexWith(ref, path); } @@ -124,7 +136,9 @@ export class ApiRepository implements Repository { return this._repository.diffBlobs(object1, object2); } - diffBetween(ref1: string, ref2: string, path: string): Promise { + diffBetween(ref1: string, ref2: string): Promise; + diffBetween(ref1: string, ref2: string, path: string): Promise; + diffBetween(ref1: string, ref2: string, path?: string): Promise { return this._repository.diffBetween(ref1, ref2, path); } @@ -183,6 +197,10 @@ export class ApiRepository implements Repository { blame(path: string): Promise { return this._repository.blame(path); } + + log(options?: LogOptions): Promise { + return this._repository.log(options); + } } export class ApiGit implements Git { diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 042add4adac..ae8eb5315bc 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -41,6 +41,7 @@ export interface Commit { readonly hash: string; readonly message: string; readonly parents: string[]; + readonly authorEmail?: string | undefined; } export interface Submodule { @@ -110,6 +111,14 @@ export interface RepositoryUIState { readonly onDidChange: Event; } +/** + * Log options. + */ +export interface LogOptions { + /** Max number of log entries to retrieve. If not specified, the default is 32. */ + readonly maxEntries?: number; +} + export interface Repository { readonly rootUri: Uri; @@ -120,6 +129,7 @@ export interface Repository { getConfigs(): Promise<{ key: string; value: string; }[]>; getConfig(key: string): Promise; setConfig(key: string, value: string): Promise; + getGlobalConfig(key: string): Promise; getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>; detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }>; @@ -131,11 +141,16 @@ export interface Repository { apply(patch: string, reverse?: boolean): Promise; diff(cached?: boolean): Promise; + diffWithHEAD(): Promise; diffWithHEAD(path: string): Promise; + diffWith(ref: string): Promise; diffWith(ref: string, path: string): Promise; + diffIndexWithHEAD(): Promise; diffIndexWithHEAD(path: string): Promise; + diffIndexWith(ref: string): Promise; diffIndexWith(ref: string, path: string): Promise; diffBlobs(object1: string, object2: string): Promise; + diffBetween(ref1: string, ref2: string): Promise; diffBetween(ref1: string, ref2: string, path: string): Promise; hashObject(data: string): Promise; @@ -156,7 +171,9 @@ export interface Repository { fetch(remote?: string, ref?: string, depth?: number): Promise; pull(unshallow?: boolean): Promise; push(remoteName?: string, branchName?: string, setUpstream?: boolean): Promise; + blame(path: string): Promise; + log(options?: LogOptions): Promise; } export interface API { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index e4b29e198ab..ea9a05d4a7e 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -12,9 +12,9 @@ import { EventEmitter } from 'events'; import iconv = require('iconv-lite'); import * as filetype from 'file-type'; import { assign, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent } from './util'; -import { CancellationToken } from 'vscode'; +import { CancellationToken, Uri } from 'vscode'; import { detectEncoding } from './encoding'; -import { Ref, RefType, Branch, Remote, GitErrorCodes } from './api/git'; +import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status } from './api/git'; const readfile = denodeify(fs.readFile); @@ -311,6 +311,8 @@ function getGitErrorCode(stderr: string): string | undefined { return undefined; } +const COMMIT_FORMAT = '%H\n%ae\n%P\n%B'; + export class Git { readonly path: string; @@ -450,6 +452,7 @@ export interface Commit { hash: string; message: string; parents: string[]; + authorEmail?: string | undefined; } export class GitStatusParser { @@ -581,13 +584,13 @@ export function parseGitmodules(raw: string): Submodule[] { } export function parseGitCommit(raw: string): Commit | null { - const match = /^([0-9a-f]{40})\n(.*)\n([^]*)$/m.exec(raw.trim()); + const match = /^([0-9a-f]{40})\n(.*)\n(.*)\n([^]*)$/m.exec(raw.trim()); if (!match) { return null; } - const parents = match[2] ? match[2].split(' ') : []; - return { hash: match[1], message: match[3], parents }; + const parents = match[3] ? match[3].split(' ') : []; + return { hash: match[1], message: match[4], parents, authorEmail: match[2] }; } interface LsTreeElement { @@ -701,6 +704,41 @@ export class Repository { }); } + async log(options?: LogOptions): Promise { + const maxEntries = options && typeof options.maxEntries === 'number' && options.maxEntries > 0 ? options.maxEntries : 32; + const args = ['log', '-' + maxEntries, `--pretty=format:${COMMIT_FORMAT}%x00%x00`]; + const gitResult = await this.run(args); + if (gitResult.exitCode) { + // An empty repo. + return []; + } + + const s = gitResult.stdout; + const result: Commit[] = []; + let index = 0; + while (index < s.length) { + let nextIndex = s.indexOf('\x00\x00', index); + if (nextIndex === -1) { + nextIndex = s.length; + } + + let entry = s.substr(index, nextIndex - index); + if (entry.startsWith('\n')) { + entry = entry.substring(1); + } + + const commit = parseGitCommit(entry); + if (!commit) { + break; + } + + result.push(commit); + index = nextIndex + 2; + } + + return result; + } + async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise { const stdout = await this.buffer(object); @@ -855,25 +893,53 @@ export class Repository { return result.stdout; } - async diffWithHEAD(path: string): Promise { + diffWithHEAD(): Promise; + diffWithHEAD(path: string): Promise; + diffWithHEAD(path?: string | undefined): Promise; + async diffWithHEAD(path?: string | undefined): Promise { + if (!path) { + return await this.diffFiles(false); + } + const args = ['diff', '--', path]; const result = await this.run(args); return result.stdout; } - async diffWith(ref: string, path: string): Promise { + diffWith(ref: string): Promise; + diffWith(ref: string, path: string): Promise; + diffWith(ref: string, path?: string | undefined): Promise; + async diffWith(ref: string, path?: string): Promise { + if (!path) { + return await this.diffFiles(false, ref); + } + const args = ['diff', ref, '--', path]; const result = await this.run(args); return result.stdout; } - async diffIndexWithHEAD(path: string): Promise { + diffIndexWithHEAD(): Promise; + diffIndexWithHEAD(path: string): Promise; + diffIndexWithHEAD(path?: string | undefined): Promise; + async diffIndexWithHEAD(path?: string): Promise { + if (!path) { + return await this.diffFiles(true); + } + const args = ['diff', '--cached', '--', path]; const result = await this.run(args); return result.stdout; } - async diffIndexWith(ref: string, path: string): Promise { + diffIndexWith(ref: string): Promise; + diffIndexWith(ref: string, path: string): Promise; + diffIndexWith(ref: string, path?: string | undefined): Promise; + async diffIndexWith(ref: string, path?: string): Promise { + if (!path) { + return await this.diffFiles(true, ref); + } + const args = ['diff', '--cached', ref, '--', path]; const result = await this.run(args); return result.stdout; @@ -885,13 +951,102 @@ export class Repository { return result.stdout; } - async diffBetween(ref1: string, ref2: string, path: string): Promise { - const args = ['diff', `${ref1}...${ref2}`, '--', path]; + diffBetween(ref1: string, ref2: string): Promise; + diffBetween(ref1: string, ref2: string, path: string): Promise; + diffBetween(ref1: string, ref2: string, path?: string | undefined): Promise; + async diffBetween(ref1: string, ref2: string, path?: string): Promise { + const range = `${ref1}...${ref2}`; + if (!path) { + return await this.diffFiles(false, range); + } + + const args = ['diff', range, '--', path]; const result = await this.run(args); return result.stdout.trim(); } + private async diffFiles(cached: boolean, ref?: string): Promise { + const args = ['diff', '--name-status', '-z', '--diff-filter=ADMR']; + if (cached) { + args.push('--cached'); + } + + if (ref) { + args.push(ref); + } + + const gitResult = await this.run(args); + if (gitResult.exitCode) { + return []; + } + + const entries = gitResult.stdout.split('\x00'); + let index = 0; + const result: Change[] = []; + + entriesLoop: + while (index < entries.length - 1) { + const change = entries[index++]; + const resourcePath = entries[index++]; + if (!change || !resourcePath) { + break; + } + + const originalUri = Uri.file(path.isAbsolute(resourcePath) ? resourcePath : path.join(this.repositoryRoot, resourcePath)); + let status: Status = Status.UNTRACKED; + + // Copy or Rename status comes with a number, e.g. 'R100'. We don't need the number, so we use only first character of the status. + switch (change[0]) { + case 'M': + status = Status.MODIFIED; + break; + + case 'A': + status = Status.INDEX_ADDED; + break; + + case 'D': + status = Status.DELETED; + break; + + // Rename contains two paths, the second one is what the file is renamed/copied to. + case 'R': + if (index >= entries.length) { + break; + } + + const newPath = entries[index++]; + if (!newPath) { + break; + } + + const uri = Uri.file(path.isAbsolute(newPath) ? newPath : path.join(this.repositoryRoot, newPath)); + result.push({ + uri, + renameUri: uri, + originalUri, + status: Status.INDEX_RENAMED + }); + + continue; + + default: + // Unknown status + break entriesLoop; + } + + result.push({ + status, + originalUri, + uri: originalUri, + renameUri: originalUri, + }); + } + + return result; + } + async getMergeBase(ref1: string, ref2: string): Promise { const args = ['merge-base', ref1, ref2]; const result = await this.run(args); @@ -1557,7 +1712,7 @@ export class Repository { } async getCommit(ref: string): Promise { - const result = await this.run(['show', '-s', '--format=%H\n%P\n%B', ref]); + const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, ref]); return parseGitCommit(result.stdout) || Promise.reject('bad commit format'); } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 8c32162f3f1..846d01945a9 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -13,7 +13,7 @@ import * as path from 'path'; import * as nls from 'vscode-nls'; import * as fs from 'fs'; import { StatusBarCommands } from './statusbar'; -import { Branch, Ref, Remote, RefType, GitErrorCodes, Status } from './api/git'; +import { Branch, Ref, Remote, RefType, GitErrorCodes, Status, LogOptions, Change } from './api/git'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -300,7 +300,8 @@ export const enum Operation { SubmoduleUpdate = 'SubmoduleUpdate', RebaseContinue = 'RebaseContinue', Apply = 'Apply', - Blame = 'Blame' + Blame = 'Blame', + Log = 'Log', } function isReadOnly(operation: Operation): boolean { @@ -727,10 +728,18 @@ export class Repository implements Disposable { return this.run(Operation.Config, () => this.repository.config('local', key)); } + getGlobalConfig(key: string): Promise { + return this.run(Operation.Config, () => this.repository.config('global', key)); + } + setConfig(key: string, value: string): Promise { return this.run(Operation.Config, () => this.repository.config('local', key, value)); } + log(options?: LogOptions): Promise { + return this.run(Operation.Log, () => this.repository.log(options)); + } + @throttle async status(): Promise { await this.run(Operation.Status); @@ -740,19 +749,31 @@ export class Repository implements Disposable { return this.run(Operation.Diff, () => this.repository.diff(cached)); } - diffWithHEAD(path: string): Promise { + diffWithHEAD(): Promise; + diffWithHEAD(path: string): Promise; + diffWithHEAD(path?: string | undefined): Promise; + diffWithHEAD(path?: string | undefined): Promise { return this.run(Operation.Diff, () => this.repository.diffWithHEAD(path)); } - diffWith(ref: string, path: string): Promise { + diffWith(ref: string): Promise; + diffWith(ref: string, path: string): Promise; + diffWith(ref: string, path?: string | undefined): Promise; + diffWith(ref: string, path?: string): Promise { return this.run(Operation.Diff, () => this.repository.diffWith(ref, path)); } - diffIndexWithHEAD(path: string): Promise { + diffIndexWithHEAD(): Promise; + diffIndexWithHEAD(path: string): Promise; + diffIndexWithHEAD(path?: string | undefined): Promise; + diffIndexWithHEAD(path?: string): Promise { return this.run(Operation.Diff, () => this.repository.diffIndexWithHEAD(path)); } - diffIndexWith(ref: string, path: string): Promise { + diffIndexWith(ref: string): Promise; + diffIndexWith(ref: string, path: string): Promise; + diffIndexWith(ref: string, path?: string | undefined): Promise; + diffIndexWith(ref: string, path?: string): Promise { return this.run(Operation.Diff, () => this.repository.diffIndexWith(ref, path)); } @@ -760,7 +781,10 @@ export class Repository implements Disposable { return this.run(Operation.Diff, () => this.repository.diffBlobs(object1, object2)); } - diffBetween(ref1: string, ref2: string, path: string): Promise { + diffBetween(ref1: string, ref2: string): Promise; + diffBetween(ref1: string, ref2: string, path: string): Promise; + diffBetween(ref1: string, ref2: string, path?: string | undefined): Promise; + diffBetween(ref1: string, ref2: string, path?: string): Promise { return this.run(Operation.Diff, () => this.repository.diffBetween(ref1, ref2, path)); } diff --git a/extensions/git/src/test/git.test.ts b/extensions/git/src/test/git.test.ts index 6b7af8c8684..e28cf10d192 100644 --- a/extensions/git/src/test/git.test.ts +++ b/extensions/git/src/test/git.test.ts @@ -177,37 +177,43 @@ suite('git', () => { suite('parseGitCommit', () => { test('single parent commit', function () { const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1 +john.doe@mail.com 8e5a374372b8393906c7e380dbb09349c5385554 This is a commit message.`; assert.deepEqual(parseGitCommit(GIT_OUTPUT_SINGLE_PARENT), { hash: '52c293a05038d865604c2284aa8698bd087915a1', message: 'This is a commit message.', - parents: ['8e5a374372b8393906c7e380dbb09349c5385554'] + parents: ['8e5a374372b8393906c7e380dbb09349c5385554'], + authorEmail: 'john.doe@mail.com', }); }); test('multiple parent commits', function () { const GIT_OUTPUT_MULTIPLE_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1 +john.doe@mail.com 8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217 This is a commit message.`; assert.deepEqual(parseGitCommit(GIT_OUTPUT_MULTIPLE_PARENTS), { hash: '52c293a05038d865604c2284aa8698bd087915a1', message: 'This is a commit message.', - parents: ['8e5a374372b8393906c7e380dbb09349c5385554', 'df27d8c75b129ab9b178b386077da2822101b217'] + parents: ['8e5a374372b8393906c7e380dbb09349c5385554', 'df27d8c75b129ab9b178b386077da2822101b217'], + authorEmail: 'john.doe@mail.com', }); }); test('no parent commits', function () { const GIT_OUTPUT_NO_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1 +john.doe@mail.com This is a commit message.`; assert.deepEqual(parseGitCommit(GIT_OUTPUT_NO_PARENTS), { hash: '52c293a05038d865604c2284aa8698bd087915a1', message: 'This is a commit message.', - parents: [] + parents: [], + authorEmail: 'john.doe@mail.com', }); }); }); diff --git a/extensions/html-language-features/.vscode/launch.json b/extensions/html-language-features/.vscode/launch.json index 28ec3d3dc89..032dc46bb43 100644 --- a/extensions/html-language-features/.vscode/launch.json +++ b/extensions/html-language-features/.vscode/launch.json @@ -17,8 +17,7 @@ ], "stopOnEntry": false, "sourceMaps": true, - "outFiles": ["${workspaceFolder}/client/out/**/*.js"], - "preLaunchTask": "npm" + "outFiles": ["${workspaceFolder}/client/out/**/*.js"] }, { "name": "Launch Tests", @@ -28,8 +27,7 @@ "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/client/out/test" ], "stopOnEntry": false, "sourceMaps": true, - "outFiles": ["${workspaceFolder}/client/out/test/**/*.js"], - "preLaunchTask": "npm" + "outFiles": ["${workspaceFolder}/client/out/test/**/*.js"] }, { "name": "Attach Language Server", diff --git a/extensions/html-language-features/.vscode/settings.json b/extensions/html-language-features/.vscode/settings.json index e46111f13be..569ac10cf8f 100644 --- a/extensions/html-language-features/.vscode/settings.json +++ b/extensions/html-language-features/.vscode/settings.json @@ -1,3 +1,6 @@ { - "editor.insertSpaces": false + "editor.insertSpaces": false, + "prettier.semi": true, + "prettier.singleQuote": true, + "prettier.printWidth": 120, } \ No newline at end of file diff --git a/extensions/html-language-features/client/src/customData.ts b/extensions/html-language-features/client/src/customData.ts index fb89f811769..8e9bf2863b6 100644 --- a/extensions/html-language-features/client/src/customData.ts +++ b/extensions/html-language-features/client/src/customData.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; -import { workspace, WorkspaceFolder } from 'vscode'; +import { workspace, WorkspaceFolder, extensions } from 'vscode'; interface ExperimentalConfig { experimental?: { @@ -12,14 +12,11 @@ interface ExperimentalConfig { }; } -export function getCustomDataPathsInAllWorkspaces(workspaceFolders: WorkspaceFolder[] | undefined) { +export function getCustomDataPathsInAllWorkspaces(workspaceFolders: WorkspaceFolder[] | undefined): string[] { const dataPaths: string[] = []; - if (!workspaceFolders) { - return { - dataPaths - }; + return dataPaths; } workspaceFolders.forEach(wf => { @@ -39,7 +36,22 @@ export function getCustomDataPathsInAllWorkspaces(workspaceFolders: WorkspaceFol } }); - return { - dataPaths - }; + return dataPaths; +} + +export function getCustomDataPathsFromAllExtensions(): string[] { + const dataPaths: string[] = []; + + for (const extension of extensions.all) { + const contributes = extension.packageJSON && extension.packageJSON.contributes; + + if (contributes && contributes.html && contributes.html.experimental.customData && Array.isArray(contributes.html.experimental.customData)) { + const relativePaths: string[] = contributes.html.customData; + relativePaths.forEach(rp => { + dataPaths.push(path.resolve(extension.extensionPath, rp)); + }); + } + } + + return dataPaths; } diff --git a/extensions/html-language-features/client/src/htmlMain.ts b/extensions/html-language-features/client/src/htmlMain.ts index a95a8a7fc25..218e05b3e32 100644 --- a/extensions/html-language-features/client/src/htmlMain.ts +++ b/extensions/html-language-features/client/src/htmlMain.ts @@ -8,12 +8,12 @@ import * as fs from 'fs'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -import { languages, ExtensionContext, IndentAction, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace } from 'vscode'; +import { languages, ExtensionContext, IndentAction, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace, SelectionRange, SelectionRangeKind } from 'vscode'; import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, RequestType, TextDocumentPositionParams } from 'vscode-languageclient'; import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared'; import { activateTagClosing } from './tagClosing'; import TelemetryReporter from 'vscode-extension-telemetry'; -import { getCustomDataPathsInAllWorkspaces } from './customData'; +import { getCustomDataPathsInAllWorkspaces, getCustomDataPathsFromAllExtensions } from './customData'; namespace TagCloseRequest { export const type: RequestType = new RequestType('html/tag'); @@ -50,7 +50,10 @@ export function activate(context: ExtensionContext) { let documentSelector = ['html', 'handlebars', 'razor']; let embeddedLanguages = { css: true, javascript: true }; - let { dataPaths } = getCustomDataPathsInAllWorkspaces(workspace.workspaceFolders); + let dataPaths = [ + ...getCustomDataPathsInAllWorkspaces(workspace.workspaceFolders), + ...getCustomDataPathsFromAllExtensions() + ]; // Options to control the language client let clientOptions: LanguageClientOptions = { @@ -84,6 +87,24 @@ export function activate(context: ExtensionContext) { } }); toDispose.push(disposable); + + documentSelector.forEach(selector => { + context.subscriptions.push(languages.registerSelectionRangeProvider(selector, { + async provideSelectionRanges(document: TextDocument, position: Position): Promise { + const textDocument = client.code2ProtocolConverter.asTextDocumentIdentifier(document); + const rawRanges = await client.sendRequest('$/textDocument/selectionRange', { textDocument, position }); + if (Array.isArray(rawRanges)) { + return rawRanges.map(r => { + return { + range: client.protocol2CodeConverter.asRange(r), + kind: SelectionRangeKind.Declaration + }; + }); + } + return []; + } + })); + }); }); languages.setLanguageConfiguration('html', { diff --git a/extensions/html-language-features/client/src/typings/ref.d.ts b/extensions/html-language-features/client/src/typings/ref.d.ts index 9c1a5df18ed..be1d1b0b776 100644 --- a/extensions/html-language-features/client/src/typings/ref.d.ts +++ b/extensions/html-language-features/client/src/typings/ref.d.ts @@ -4,3 +4,4 @@ *--------------------------------------------------------------------------------------------*/ /// +/// diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 812b6104c1b..0fc7ed3ce11 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -1,4 +1,5 @@ { + "enableProposedApi": true, "name": "html-language-features", "displayName": "%displayName%", "description": "%description%", @@ -116,14 +117,18 @@ "force", "force-aligned", "force-expand-multiline", - "aligned-multiple" + "aligned-multiple", + "preserve", + "preserve-aligned" ], "enumDescriptions": [ "%html.format.wrapAttributes.auto%", "%html.format.wrapAttributes.force%", "%html.format.wrapAttributes.forcealign%", "%html.format.wrapAttributes.forcemultiline%", - "%html.format.wrapAttributes.alignedmultiple%" + "%html.format.wrapAttributes.alignedmultiple%", + "%html.format.wrapAttributes.preserve%", + "%html.format.wrapAttributes.preservealigned%" ], "description": "%html.format.wrapAttributes.desc%" }, diff --git a/extensions/html-language-features/package.nls.json b/extensions/html-language-features/package.nls.json index c6923ce881e..343a5a7e2d9 100644 --- a/extensions/html-language-features/package.nls.json +++ b/extensions/html-language-features/package.nls.json @@ -17,6 +17,8 @@ "html.format.wrapAttributes.forcealign": "Wrap each attribute except first and keep aligned.", "html.format.wrapAttributes.forcemultiline": "Wrap each attribute.", "html.format.wrapAttributes.alignedmultiple": "Wrap when line length is exceeded, align attributes vertically.", + "html.format.wrapAttributes.preserve": "Preserve wrapping of attributes", + "html.format.wrapAttributes.preservealigned": "Preserve wrapping of attributes but align.", "html.suggest.angular1.desc": "Controls whether the built-in HTML language support suggests Angular V1 tags and properties.", "html.suggest.ionic.desc": "Controls whether the built-in HTML language support suggests Ionic tags, properties and values.", "html.suggest.html5.desc": "Controls whether the built-in HTML language support suggests HTML5 tags, properties and values.", diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 77ceb8edc5d..e631b8e2db0 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -9,8 +9,8 @@ }, "main": "./out/htmlServerMain", "dependencies": { - "vscode-css-languageservice": "^3.0.13-next.6", - "vscode-html-languageservice": "^2.1.11-next.3", + "vscode-css-languageservice": "^3.0.13-next.10", + "vscode-html-languageservice": "^2.1.11-next.9", "vscode-languageserver": "^5.1.0", "vscode-languageserver-types": "^3.13.0", "vscode-nls": "^4.0.0", diff --git a/extensions/html-language-features/server/src/customData.ts b/extensions/html-language-features/server/src/customData.ts new file mode 100644 index 00000000000..673e4a4ab9d --- /dev/null +++ b/extensions/html-language-features/server/src/customData.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IHTMLDataProvider, HTMLDataProvider } from 'vscode-html-languageservice'; +import * as fs from 'fs'; + +export function getDataProviders(dataPaths?: string[]): IHTMLDataProvider[] { + if (!dataPaths) { + return []; + } + + const providers: IHTMLDataProvider[] = []; + + dataPaths.forEach((path, i) => { + try { + if (fs.existsSync(path)) { + const htmlData = JSON.parse(fs.readFileSync(path, 'utf-8')); + + providers.push(new HTMLDataProvider(`customProvider${i}`, htmlData)); + } + } catch (err) { + console.log(`Failed to load tag from ${path}`); + } + }); + + return providers; +} \ No newline at end of file diff --git a/extensions/html-language-features/server/src/htmlServerMain.ts b/extensions/html-language-features/server/src/htmlServerMain.ts index cec1d1da31e..c904afe354c 100644 --- a/extensions/html-language-features/server/src/htmlServerMain.ts +++ b/extensions/html-language-features/server/src/htmlServerMain.ts @@ -11,7 +11,6 @@ import { } from 'vscode-languageserver'; import { TextDocument, Diagnostic, DocumentLink, SymbolInformation } from 'vscode-languageserver-types'; import { getLanguageModes, LanguageModes, Settings } from './modes/languageModes'; -import * as fs from 'fs'; import { format } from './modes/formatting'; import { pushAll } from './utils/arrays'; @@ -20,8 +19,7 @@ import uri from 'vscode-uri'; import { formatError, runSafe, runSafeAsync } from './utils/runner'; import { getFoldingRanges } from './modes/htmlFolding'; -import { parseHTMLData } from './utils/tagDefinitions'; -import { HTMLData } from 'vscode-html-languageservice'; +import { getDataProviders } from './customData'; namespace TagCloseRequest { export const type: RequestType = new RequestType('html/tag'); @@ -92,37 +90,14 @@ connection.onInitialize((params: InitializeParams): InitializeResult => { } const dataPaths: string[] = params.initializationOptions.dataPaths; - - let allHtmlData: HTMLData = { - tags: [], - globalAttributes: [], - valueSetMap: {} - }; - - if (dataPaths) { - dataPaths.forEach(path => { - try { - if (fs.existsSync(path)) { - const htmlData = parseHTMLData(fs.readFileSync(path, 'utf-8')); - if (htmlData.tags) { - allHtmlData.tags = allHtmlData.tags!.concat(htmlData.tags); - } - if (htmlData.globalAttributes) { - allHtmlData.globalAttributes = allHtmlData.globalAttributes!.concat(htmlData.globalAttributes); - } - } - } catch (err) { - console.log(`Failed to laod tag from ${path}`); - } - }); - } + const providers = getDataProviders(dataPaths); const workspace = { get settings() { return globalSettings; }, get folders() { return workspaceFolders; } }; - languageModes = getLanguageModes(initializationOptions ? initializationOptions.embeddedLanguages : { css: true, javascript: true }, workspace, allHtmlData); + languageModes = getLanguageModes(initializationOptions ? initializationOptions.embeddedLanguages : { css: true, javascript: true }, workspace, providers); documents.onDidClose(e => { languageModes.onDocumentRemoved(e.document); @@ -480,6 +455,21 @@ connection.onFoldingRanges((params, token) => { }, null, `Error while computing folding regions for ${params.textDocument.uri}`, token); }); +connection.onRequest('$/textDocument/selectionRange', async (params, token) => { + return runSafe(() => { + const document = documents.get(params.textDocument.uri); + const position: Position = params.position; + + if (document) { + const htmlMode = languageModes.getMode('html'); + if (htmlMode && htmlMode.doSelection) { + return htmlMode.doSelection(document, position); + } + } + return Promise.resolve(null); + }, null, `Error while computing selection ranges for ${params.textDocument.uri}`, token); +}); + // Listen on the connection connection.listen(); \ No newline at end of file diff --git a/extensions/html-language-features/server/src/modes/htmlMode.ts b/extensions/html-language-features/server/src/modes/htmlMode.ts index 65756c5ee14..00a690f56eb 100644 --- a/extensions/html-language-features/server/src/modes/htmlMode.ts +++ b/extensions/html-language-features/server/src/modes/htmlMode.ts @@ -15,6 +15,9 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace: getId() { return 'html'; }, + doSelection(document: TextDocument, position: Position): Range[] { + return htmlLanguageService.getSelectionRanges(document, position); + }, doComplete(document: TextDocument, position: Position, settings = workspace.settings) { let options = settings && settings.html && settings.html.suggest; let doAutoComplete = settings && settings.html && settings.html.autoClosingTags; diff --git a/extensions/html-language-features/server/src/modes/languageModes.ts b/extensions/html-language-features/server/src/modes/languageModes.ts index e0b073bdaef..048076ed70d 100644 --- a/extensions/html-language-features/server/src/modes/languageModes.ts +++ b/extensions/html-language-features/server/src/modes/languageModes.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getLanguageService as getHTMLLanguageService, DocumentContext, HTMLData } from 'vscode-html-languageservice'; +import { getLanguageService as getHTMLLanguageService, DocumentContext, IHTMLDataProvider } from 'vscode-html-languageservice'; import { CompletionItem, Location, SignatureHelp, Definition, TextEdit, TextDocument, Diagnostic, DocumentLink, Range, Hover, DocumentHighlight, CompletionList, Position, FormattingOptions, SymbolInformation, FoldingRange @@ -31,6 +31,7 @@ export interface Workspace { export interface LanguageMode { getId(): string; + doSelection?: (document: TextDocument, position: Position) => Range[]; doValidation?: (document: TextDocument, settings?: Settings) => Diagnostic[]; doComplete?: (document: TextDocument, position: Position, settings?: Settings) => CompletionList; doResolve?: (document: TextDocument, item: CompletionItem) => CompletionItem; @@ -65,9 +66,8 @@ export interface LanguageModeRange extends Range { attributeValue?: boolean; } -export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }, workspace: Workspace, customData?: HTMLData): LanguageModes { - const customDataCollections = customData ? [customData] : []; - const htmlLanguageService = getHTMLLanguageService({ customDataCollections }); +export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }, workspace: Workspace, customDataProviders?: IHTMLDataProvider[]): LanguageModes { + const htmlLanguageService = getHTMLLanguageService({ customDataProviders }); let documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(htmlLanguageService, document)); diff --git a/extensions/html-language-features/server/src/utils/tagDefinitions.ts b/extensions/html-language-features/server/src/utils/tagDefinitions.ts deleted file mode 100644 index 110a2acc5d7..00000000000 --- a/extensions/html-language-features/server/src/utils/tagDefinitions.ts +++ /dev/null @@ -1,10 +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 { HTMLData } from 'vscode-html-languageservice'; - -export function parseHTMLData(source: string): HTMLData { - return JSON.parse(source); -} \ No newline at end of file diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index 3bb00545094..e0a65914a11 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -229,18 +229,18 @@ supports-color@5.4.0: dependencies: has-flag "^3.0.0" -vscode-css-languageservice@^3.0.13-next.6: - version "3.0.13-next.6" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-3.0.13-next.6.tgz#0329ea1da29ca84d1821cd32ee10bca0ee4c50cd" - integrity sha512-wC8zaFWHNnqIaOT4LXByy3NyTl916uHxGy3U3cpV7Gw7F8ENylSGM2RAO+l7NohIbP0WZlet441HEwSbb+bZbQ== +vscode-css-languageservice@^3.0.13-next.10: + version "3.0.13-next.10" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-3.0.13-next.10.tgz#f5822e832b06e1e91ec96c528bab83bb07251c35" + integrity sha512-zKwzo3GVhrAllYDM4afL8q1XCHixsI8tP3SyLrWGzp0Nc9P+bbjKQeC26VcaOb0dtkgfpB/vfBPf+4yOs4s/pw== dependencies: vscode-languageserver-types "^3.13.0" vscode-nls "^4.0.0" -vscode-html-languageservice@^2.1.11-next.3: - version "2.1.11-next.3" - resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-2.1.11-next.3.tgz#f8eaee042c161f47fb71850c558428d0d91c881f" - integrity sha512-621+f1nbRvMgLyvW1Aa9shZ9r9qBIXMi4fF8o0voooHjIggrIbstUpYmaQIFRNill3b9HCNdluTZnAQlQGU6Ew== +vscode-html-languageservice@^2.1.11-next.9: + version "2.1.11-next.9" + resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-2.1.11-next.9.tgz#1471b945351a4b179bc9b995e9617f07ab12d647" + integrity sha512-iSQPafYHDv62dDpwXVmaJzuS3y8yuehPwtWEBC9QysNBm5LxrRWy293pC7b9CehVly6mDnrLVXSRfgwAtg9xzA== dependencies: vscode-languageserver-types "^3.13.0" vscode-nls "^4.0.0" diff --git a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json index 869f8dafa44..5d6df3a855e 100644 --- a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json +++ b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/76cfeabda164d2d132222af72e0a5f991e26c51a", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/444971648e9e1e41c54c62f41d1310f2816965c4", "name": "JavaScript (with React support)", "scopeName": "source.js", "patterns": [ @@ -690,6 +690,12 @@ { "include": "#string" }, + { + "include": "#numeric-literal" + }, + { + "include": "#regex" + }, { "include": "#object-binding-pattern" }, @@ -712,6 +718,12 @@ { "include": "#string" }, + { + "include": "#numeric-literal" + }, + { + "include": "#regex" + }, { "include": "#object-binding-pattern-const" }, @@ -986,6 +998,12 @@ { "include": "#string" }, + { + "include": "#numeric-literal" + }, + { + "include": "#regex" + }, { "include": "#parameter-object-binding-pattern" }, @@ -2462,8 +2480,8 @@ }, { "name": "meta.object.member.js meta.object-literal.key.js", - "begin": "(?=[\\'\\\"])", - "end": "(?=:)|((?<=[\\'\\\"])(?=\\s*[\\(\\<]))", + "begin": "(?=[\\'\\\"\\`])", + "end": "(?=:)|((?<=[\\'\\\"\\`])(?=((\\s*[\\(\\<,}])|(\\s+(as)\\s+))))", "patterns": [ { "include": "#comment" @@ -2473,9 +2491,22 @@ } ] }, + { + "name": "meta.object.member.js meta.object-literal.key.js", + "begin": "(?x)(?=(\\b(? { + client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociation(context)); + }); + + documentSelector.forEach(selector => { + toDispose.push(languages.registerSelectionRangeProvider(selector, { + async provideSelectionRanges(document: TextDocument, position: Position): Promise { + const textDocument = client.code2ProtocolConverter.asTextDocumentIdentifier(document); + const rawRanges = await client.sendRequest('$/textDocument/selectionRange', { textDocument, position }); + if (Array.isArray(rawRanges)) { + return rawRanges.map(r => { + return { + range: client.protocol2CodeConverter.asRange(r), + kind: SelectionRangeKind.Declaration + }; + }); + } + return []; + } + })); + }); }); + + let languageConfiguration: LanguageConfiguration = { wordPattern: /("(?:[^\\\"]*(?:\\.)?)*"?)|[^\s{}\[\],:]+/, indentationRules: { diff --git a/extensions/json-language-features/client/src/typings/ref.d.ts b/extensions/json-language-features/client/src/typings/ref.d.ts index 9c1a5df18ed..be1d1b0b776 100644 --- a/extensions/json-language-features/client/src/typings/ref.d.ts +++ b/extensions/json-language-features/client/src/typings/ref.d.ts @@ -4,3 +4,4 @@ *--------------------------------------------------------------------------------------------*/ /// +/// diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 1eb1e6e2f88..6e982dac417 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -14,6 +14,7 @@ "onLanguage:jsonc" ], "main": "./client/out/jsonMain", + "enableProposedApi": true, "scripts": { "compile": "gulp compile-extension:json-language-features-client compile-extension:json-language-features-server", "watch": "gulp watch-extension:json-language-features-client watch-extension:json-language-features-server", @@ -107,4 +108,4 @@ "devDependencies": { "@types/node": "^8.10.25" } -} +} \ No newline at end of file diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index db0182fa088..0868d9aa049 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -14,7 +14,7 @@ "dependencies": { "jsonc-parser": "^2.0.2", "request-light": "^0.2.4", - "vscode-json-languageservice": "^3.2.1", + "vscode-json-languageservice": "^3.3.0-next.0", "vscode-languageserver": "^5.1.0", "vscode-nls": "^4.0.0", "vscode-uri": "^1.0.6" diff --git a/extensions/json-language-features/server/src/jsonServerMain.ts b/extensions/json-language-features/server/src/jsonServerMain.ts index e3b2dcb5f8f..5c3539454e4 100644 --- a/extensions/json-language-features/server/src/jsonServerMain.ts +++ b/extensions/json-language-features/server/src/jsonServerMain.ts @@ -427,5 +427,16 @@ connection.onFoldingRanges((params, token) => { }, null, `Error while computing folding ranges for ${params.textDocument.uri}`, token); }); +connection.onRequest('$/textDocument/selectionRange', async (params, token) => { + return runSafe(() => { + const document = documents.get(params.textDocument.uri); + if (document) { + const jsonDocument = getJSONDocument(document); + return languageService.getSelectionRanges(document, params.position, jsonDocument); + } + return []; + }, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token); +}); + // Listen on the connection connection.listen(); diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index 012e346360a..1c933ebfb3c 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -73,10 +73,10 @@ request-light@^0.2.4: https-proxy-agent "^2.2.1" vscode-nls "^4.0.0" -vscode-json-languageservice@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-3.2.1.tgz#991d51128ebd81c5525d0578cabfa5b03e3cba2a" - integrity sha512-ee9MJ70/xR55ywvm0bZsDLhA800HCRE27AYgMNTU14RSg20Y+ngHdQnUt6OmiTXrQDI/7sne6QUOtHIN0hPQYA== +vscode-json-languageservice@^3.3.0-next.0: + version "3.3.0-next.0" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-3.3.0-next.0.tgz#c17db95d0eacc24f80d3b3f120ab5e03943769a0" + integrity sha512-YZXL3yHzbr0/Ar5dGdeM/f5Y0l41z/Y4QSQTdL3Hl3ScuY76IPcDEnf7iuk9yx+QoPfEHFCBDv5Rg6XVcMl8Tg== dependencies: jsonc-parser "^2.0.2" vscode-languageserver-types "^3.13.0" diff --git a/extensions/markdown-language-features/src/commands/openDocumentLink.ts b/extensions/markdown-language-features/src/commands/openDocumentLink.ts index 42dfe67df25..ea929f484f6 100644 --- a/extensions/markdown-language-features/src/commands/openDocumentLink.ts +++ b/extensions/markdown-language-features/src/commands/openDocumentLink.ts @@ -25,7 +25,7 @@ export class OpenDocumentLinkCommand implements Command { path: string, fragment: string ): vscode.Uri { - return vscode.Uri.parse(`command:${OpenDocumentLinkCommand.id}?${encodeURIComponent(JSON.stringify({ path, fragment }))}`); + return vscode.Uri.parse(`command:${OpenDocumentLinkCommand.id}?${encodeURIComponent(JSON.stringify({ path: encodeURIComponent(path), fragment }))}`); } public constructor( diff --git a/extensions/markdown-language-features/src/features/documentLinkProvider.ts b/extensions/markdown-language-features/src/features/documentLinkProvider.ts index f4e1bd0a9d2..b35f3f0d787 100644 --- a/extensions/markdown-language-features/src/features/documentLinkProvider.ts +++ b/extensions/markdown-language-features/src/features/documentLinkProvider.ts @@ -50,8 +50,27 @@ function matchAll( return out; } +function extractDocumentLink( + document: vscode.TextDocument, + base: string, + pre: number, + link: string, + matchIndex: number | undefined +): vscode.DocumentLink | undefined { + const offset = (matchIndex || 0) + pre; + const linkStart = document.positionAt(offset); + const linkEnd = document.positionAt(offset + link.length); + try { + return new vscode.DocumentLink( + new vscode.Range(linkStart, linkEnd), + normalizeLink(document, link, base)); + } catch (e) { + return undefined; + } +} + export default class LinkProvider implements vscode.DocumentLinkProvider { - private readonly linkPattern = /(\[[^\]]*\]\(\s*)((([^\s\(\)]|\(\S*?\))+))\s*(".*?")?\)/g; + private readonly linkPattern = /(\[((!\[[^\]]*?\]\(\s*)([^\s\(\)]+?)\s*\)\]|[^\]]*\])\(\s*)(([^\s\(\)]|\(\S*?\))+)\s*(".*?")?\)/g; private readonly referenceLinkPattern = /(\[([^\]]+)\]\[\s*?)([^\s\]]*?)\]/g; private readonly definitionPattern = /^([\t ]*\[([^\]]+)\]:\s*)(\S+)/gm; @@ -73,20 +92,15 @@ export default class LinkProvider implements vscode.DocumentLinkProvider { ): vscode.DocumentLink[] { const results: vscode.DocumentLink[] = []; for (const match of matchAll(this.linkPattern, text)) { - const pre = match[1]; - const link = match[2]; - const offset = (match.index || 0) + pre.length; - const linkStart = document.positionAt(offset); - const linkEnd = document.positionAt(offset + link.length); - try { - results.push(new vscode.DocumentLink( - new vscode.Range(linkStart, linkEnd), - normalizeLink(document, link, base))); - } catch (e) { - // noop + const matchImage = match[4] && extractDocumentLink(document, base, match[3].length + 1, match[4], match.index); + if (matchImage) { + results.push(matchImage); + } + const matchLink = extractDocumentLink(document, base, match[1].length, match[5], match.index); + if (matchLink) { + results.push(matchLink); } } - return results; } @@ -159,4 +173,4 @@ export default class LinkProvider implements vscode.DocumentLinkProvider { } return out; } -} +} \ No newline at end of file diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index ac2743a487d..90e7284cf2b 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -131,7 +131,7 @@ export class MarkdownEngine { const config = this.getConfig(document.uri); const engine = await this.getEngine(config); return engine.renderer.render(this.tokenize(document, config, engine), { - ...(await getMarkdownOptions(() => engine)), + ...(engine as any).options, ...config }, {}); } diff --git a/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts b/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts index 5a1d0c87880..f6309a027d3 100644 --- a/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts +++ b/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts @@ -103,6 +103,33 @@ suite('markdown.DocumentLinkProvider', () => { assertRangeEqual(link1.range, new vscode.Range(0, 10, 0, 14)); assertRangeEqual(link2.range, new vscode.Range(0, 23, 0, 28)); }); + + // #49238 + test('should handle hyperlinked images', () => { + { + const links = getLinksForFile('[![alt text](image.jpg)](https://example.com)'); + assert.strictEqual(links.length, 2); + const [link1, link2] = links; + assertRangeEqual(link1.range, new vscode.Range(0,13,0,22)); + assertRangeEqual(link2.range, new vscode.Range(0,25,0,44)); + } + { + const links = getLinksForFile('[![a]( whitespace.jpg )]( https://whitespace.com )'); + assert.strictEqual(links.length, 2); + const [link1, link2] = links; + assertRangeEqual(link1.range, new vscode.Range(0,7,0,21)); + assertRangeEqual(link2.range, new vscode.Range(0,26,0,48)); + } + { + const links = getLinksForFile('[![a](img1.jpg)](file1.txt) text [![a](img2.jpg)](file2.txt)'); + assert.strictEqual(links.length, 4); + const [link1, link2, link3, link4] = links; + assertRangeEqual(link1.range, new vscode.Range(0,6,0,14)); + assertRangeEqual(link2.range, new vscode.Range(0,17,0,26)); + assertRangeEqual(link3.range, new vscode.Range(0,39,0,47)); + assertRangeEqual(link4.range, new vscode.Range(0,50,0,59)); + } + }); }); diff --git a/extensions/merge-conflict/src/documentTracker.ts b/extensions/merge-conflict/src/documentTracker.ts index 97eeae6f87e..41be7a803e2 100644 --- a/extensions/merge-conflict/src/documentTracker.ts +++ b/extensions/merge-conflict/src/documentTracker.ts @@ -127,7 +127,7 @@ export default class DocumentMergeConflictTracker implements vscode.Disposable, } private getCacheKey(document: vscode.TextDocument): string | null { - if (document.uri && document.uri) { + if (document.uri) { return document.uri.toString(); } diff --git a/extensions/python/package.json b/extensions/python/package.json index 48151bd6245..c81942af764 100644 --- a/extensions/python/package.json +++ b/extensions/python/package.json @@ -10,7 +10,7 @@ "contributes": { "languages": [{ "id": "python", - "extensions": [ ".py", ".rpy", ".pyw", ".cpy", ".gyp", ".gypi", ".snakefile", ".smk"], + "extensions": [ ".py", ".rpy", ".pyw", ".cpy", ".gyp", ".gypi", ".snakefile", ".smk", ".pyi"], "aliases": [ "Python", "py" ], "firstLine": "^#!\\s*/.*\\bpython[0-9.-]*\\b", "configuration": "./language-configuration.json" diff --git a/extensions/typescript-basics/cgmanifest.json b/extensions/typescript-basics/cgmanifest.json index d2905a195d6..e604d6ec27b 100644 --- a/extensions/typescript-basics/cgmanifest.json +++ b/extensions/typescript-basics/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "TypeScript-TmLanguage", "repositoryUrl": "https://github.com/Microsoft/TypeScript-TmLanguage", - "commitHash": "76cfeabda164d2d132222af72e0a5f991e26c51a" + "commitHash": "444971648e9e1e41c54c62f41d1310f2816965c4" } }, "license": "MIT", diff --git a/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json b/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json index 0436a2c25e1..455f0756a72 100644 --- a/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json +++ b/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/76cfeabda164d2d132222af72e0a5f991e26c51a", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/444971648e9e1e41c54c62f41d1310f2816965c4", "name": "TypeScript", "scopeName": "source.ts", "patterns": [ @@ -687,6 +687,12 @@ { "include": "#string" }, + { + "include": "#numeric-literal" + }, + { + "include": "#regex" + }, { "include": "#object-binding-pattern" }, @@ -709,6 +715,12 @@ { "include": "#string" }, + { + "include": "#numeric-literal" + }, + { + "include": "#regex" + }, { "include": "#object-binding-pattern-const" }, @@ -983,6 +995,12 @@ { "include": "#string" }, + { + "include": "#numeric-literal" + }, + { + "include": "#regex" + }, { "include": "#parameter-object-binding-pattern" }, @@ -2459,8 +2477,8 @@ }, { "name": "meta.object.member.ts meta.object-literal.key.ts", - "begin": "(?=[\\'\\\"])", - "end": "(?=:)|((?<=[\\'\\\"])(?=\\s*[\\(\\<]))", + "begin": "(?=[\\'\\\"\\`])", + "end": "(?=:)|((?<=[\\'\\\"\\`])(?=((\\s*[\\(\\<,}])|(\\s+(as)\\s+))))", "patterns": [ { "include": "#comment" @@ -2470,9 +2488,22 @@ } ] }, + { + "name": "meta.object.member.ts meta.object-literal.key.ts", + "begin": "(?x)(?=(\\b(?)", + "captures": { + "1": { + "name": "meta.brace.angle.ts" + }, + "2": { + "name": "storage.modifier.ts" + }, + "3": { + "name": "meta.brace.angle.ts" + } + } + }, { "name": "cast.expr.ts", "begin": "(?:(?*?\\&\\|\\^]|[^_$[:alnum:]](?:\\+\\+|\\-\\-)|[^\\+]\\+|[^\\-]\\-))\\s*(<)(?! | undefined; + let res: ServerResponse.Response | undefined; try { res = await client.execute('projectInfo', { file, needFileNameList: false }, nulToken); } catch { diff --git a/extensions/typescript-language-features/src/features/completions.ts b/extensions/typescript-language-features/src/features/completions.ts index f07e335daeb..c0020fdfef0 100644 --- a/extensions/typescript-language-features/src/features/completions.ts +++ b/extensions/typescript-language-features/src/features/completions.ts @@ -638,25 +638,23 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider try { const args: Proto.FileLocationRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position); const response = await this.client.execute('quickinfo', args, token); - if (response.type !== 'response' || !response.body) { - return true; + if (response.type === 'response' && response.body) { + switch (response.body.kind) { + case 'var': + case 'let': + case 'const': + case 'alias': + return false; + } } - - switch (response.body.kind) { - case 'var': - case 'let': - case 'const': - case 'alias': - return false; - } - } catch (e) { - return true; + } catch { + // Noop } - // Don't complete function call if there is already something that looks like a funciton call + // Don't complete function call if there is already something that looks like a function call // https://github.com/Microsoft/vscode/issues/18131 const after = document.lineAt(position.line).text.slice(position.character); - return after.match(/^\s*\(/g) === null; + return after.match(/^[a-z_$0-9]*\s*\(/gi) === null; } } diff --git a/extensions/typescript-language-features/src/features/fileConfigurationManager.ts b/extensions/typescript-language-features/src/features/fileConfigurationManager.ts index 794396595b2..676e80bafd1 100644 --- a/extensions/typescript-language-features/src/features/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/features/fileConfigurationManager.ts @@ -9,6 +9,7 @@ import { ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { isTypeScriptDocument } from '../utils/languageModeIds'; import { ResourceMap } from '../utils/resourceMap'; +import { Disposable } from '../utils/dispose'; function objsAreEqual(a: T, b: T): boolean { @@ -33,27 +34,20 @@ function areFileConfigurationsEqual(a: FileConfiguration, b: FileConfiguration): ); } -export default class FileConfigurationManager { - private onDidCloseTextDocumentSub: vscode.Disposable | undefined; +export default class FileConfigurationManager extends Disposable { private readonly formatOptions = new ResourceMap>(); public constructor( private readonly client: ITypeScriptServiceClient ) { - this.onDidCloseTextDocumentSub = vscode.workspace.onDidCloseTextDocument(textDocument => { + super(); + vscode.workspace.onDidCloseTextDocument(textDocument => { // When a document gets closed delete the cached formatting options. // This is necessary since the tsserver now closed a project when its // last file in it closes which drops the stored formatting options // as well. this.formatOptions.delete(textDocument.uri); - }); - } - - public dispose() { - if (this.onDidCloseTextDocumentSub) { - this.onDidCloseTextDocumentSub.dispose(); - this.onDidCloseTextDocumentSub = undefined; - } + }, undefined, this._disposables); } public async ensureConfigurationForDocument( @@ -189,7 +183,7 @@ export default class FileConfigurationManager { allowTextChangesInNewFiles: document.uri.scheme === 'file', providePrefixAndSuffixTextForRename: true, allowRenameOfImportPath: true, - } as Proto.UserPreferences; + }; } } diff --git a/extensions/typescript-language-features/src/features/jsDocCompletions.ts b/extensions/typescript-language-features/src/features/jsDocCompletions.ts index e84b27ed2b8..36f7a751522 100644 --- a/extensions/typescript-language-features/src/features/jsDocCompletions.ts +++ b/extensions/typescript-language-features/src/features/jsDocCompletions.ts @@ -112,9 +112,10 @@ export function templateToSnippet(template: string): vscode.SnippetString { export function register( selector: vscode.DocumentSelector, + modeId: string, client: ITypeScriptServiceClient, ): vscode.Disposable { - return new ConfigurationDependentRegistration('jsDocCompletion', 'enabled', () => { + return new ConfigurationDependentRegistration(modeId, 'suggest.completeJSDocs', () => { return vscode.languages.registerCompletionItemProvider(selector, new JsDocCompletionProvider(client), '*'); diff --git a/extensions/typescript-language-features/src/features/quickFix.ts b/extensions/typescript-language-features/src/features/quickFix.ts index ec98820c7f5..91c3f3b3265 100644 --- a/extensions/typescript-language-features/src/features/quickFix.ts +++ b/extensions/typescript-language-features/src/features/quickFix.ts @@ -269,9 +269,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider { arguments: [tsAction], title: '' }; - if (tsAction.fixName === 'spelling') { - codeAction.isPreferred = true; - } + codeAction.isPreferred = isPreferredFix(tsAction); return codeAction; } @@ -305,6 +303,21 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider { } } +const preferredFixes = new Set([ + 'annotateWithTypeFromJSDoc', + 'constructorForDerivedNeedSuperCall', + 'extendsInterfaceBecomesImplements', + 'fixAwaitInSyncFunction', + 'fixClassIncorrectlyImplementsInterface', + 'fixUnreachableCode', + 'forgottenThisPropertyAccess', + 'spelling', + 'unusedIdentifier', +]); +function isPreferredFix(tsAction: Proto.CodeFixAction): boolean { + return preferredFixes.has(tsAction.fixName); +} + export function register( selector: vscode.DocumentSelector, client: ITypeScriptServiceClient, diff --git a/extensions/typescript-language-features/src/features/refactor.ts b/extensions/typescript-language-features/src/features/refactor.ts index 5f28e8e398b..bad25c983c4 100644 --- a/extensions/typescript-language-features/src/features/refactor.ts +++ b/extensions/typescript-language-features/src/features/refactor.ts @@ -188,6 +188,7 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { command: ApplyRefactoringCommand.ID, arguments: [document, file, info.name, action.name, rangeOrSelection], }; + codeAction.isPreferred = TypeScriptRefactorProvider.isPreferred(action); return codeAction; } @@ -209,6 +210,15 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { } return vscode.CodeActionKind.Refactor; } + + private static isPreferred( + action: Proto.RefactorActionInfo + ): boolean { + if (action.name.startsWith('constant_')) { + return action.name.endsWith('scope_0'); + } + return false; + } } export function register( diff --git a/extensions/typescript-language-features/src/features/rename.ts b/extensions/typescript-language-features/src/features/rename.ts index 55e6c819a08..aff2c508c53 100644 --- a/extensions/typescript-language-features/src/features/rename.ts +++ b/extensions/typescript-language-features/src/features/rename.ts @@ -79,7 +79,7 @@ class TypeScriptRenameProvider implements vscode.RenameProvider { document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken - ): Promise | undefined> { + ): Promise | undefined> { const file = this.client.toOpenedFilePath(document); if (!file) { return undefined; diff --git a/extensions/typescript-language-features/src/features/task.ts b/extensions/typescript-language-features/src/features/task.ts index c71f3c4435d..550921e1488 100644 --- a/extensions/typescript-language-features/src/features/task.ts +++ b/extensions/typescript-language-features/src/features/task.ts @@ -11,6 +11,7 @@ import { ITypeScriptServiceClient } from '../typescriptService'; import { Lazy } from '../utils/lazy'; import { isImplicitProjectConfigFile } from '../utils/tsconfig'; import TsConfigProvider, { TSConfig } from '../utils/tsconfigProvider'; +import { Disposable } from '../utils/dispose'; const localize = nls.loadMessageBundle(); @@ -197,7 +198,7 @@ class TscTaskProvider implements vscode.TaskProvider { project.workspaceFolder || vscode.TaskScope.Workspace, localize('buildAndWatchTscLabel', 'watch - {0}', label), 'tsc', - new vscode.ShellExecution(command, ['--watch', ...args]), + new vscode.ShellExecution(command, [...args, '--watch']), '$tsc-watch'); watchTask.group = vscode.TaskGroup.Build; watchTask.isBackground = true; @@ -244,23 +245,24 @@ class TscTaskProvider implements vscode.TaskProvider { /** * Manages registrations of TypeScript task providers with VS Code. */ -export default class TypeScriptTaskProviderManager { +export default class TypeScriptTaskProviderManager extends Disposable { private taskProviderSub: vscode.Disposable | undefined = undefined; - private readonly disposables: vscode.Disposable[] = []; constructor( private readonly client: Lazy ) { - vscode.workspace.onDidChangeConfiguration(this.onConfigurationChanged, this, this.disposables); + super(); + vscode.workspace.onDidChangeConfiguration(this.onConfigurationChanged, this, this._disposables); this.onConfigurationChanged(); } dispose() { + super.dispose(); + if (this.taskProviderSub) { this.taskProviderSub.dispose(); this.taskProviderSub = undefined; } - this.disposables.forEach(x => x.dispose()); } private onConfigurationChanged() { @@ -272,4 +274,4 @@ export default class TypeScriptTaskProviderManager { this.taskProviderSub = vscode.workspace.registerTaskProvider('typescript', new TscTaskProvider(this.client)); } } -} \ No newline at end of file +} diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index 3a927cf777d..6f122fcb633 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -65,7 +65,7 @@ export default class LanguageProvider extends Disposable { this._register((await import('./features/hover')).register(selector, this.client)); this._register((await import('./features/implementations')).register(selector, this.client)); this._register((await import('./features/implementationsCodeLens')).register(selector, this.description.id, this.client, cachedResponse)); - this._register((await import('./features/jsDocCompletions')).register(selector, this.client)); + this._register((await import('./features/jsDocCompletions')).register(selector, this.description.id, this.client)); this._register((await import('./features/organizeImports')).register(selector, this.client, this.commandManager, this.fileConfigurationManager, this.telemetryReporter)); this._register((await import('./features/quickFix')).register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.client.diagnosticsManager, this.telemetryReporter)); this._register((await import('./features/fixAll')).register(selector, this.client, this.fileConfigurationManager, this.client.diagnosticsManager)); diff --git a/extensions/typescript-language-features/src/test/cachedResponse.test.ts b/extensions/typescript-language-features/src/test/cachedResponse.test.ts index de1003f284f..919d5d4d03c 100644 --- a/extensions/typescript-language-features/src/test/cachedResponse.test.ts +++ b/extensions/typescript-language-features/src/test/cachedResponse.test.ts @@ -8,7 +8,7 @@ import 'mocha'; import * as vscode from 'vscode'; import * as Proto from '../protocol'; import { CachedResponse } from '../tsServer/cachedResponse'; -import { ServerResponse, CancelledResponse } from '../typescriptService'; +import { ServerResponse } from '../typescriptService'; suite('CachedResponse', () => { test('should cache simple response for same document', async () => { @@ -36,12 +36,12 @@ suite('CachedResponse', () => { const doc = await createTextDocument(); const response = new CachedResponse(); - const cancelledResponder = createEventualResponder(); + const cancelledResponder = createEventualResponder(); const result1 = response.execute(doc, () => cancelledResponder.promise); const result2 = response.execute(doc, respondWith('test-0')); const result3 = response.execute(doc, respondWith('test-1')); - cancelledResponder.resolve(new CancelledResponse('cancelled')); + cancelledResponder.resolve(new ServerResponse.Cancelled('cancelled')); assert.strictEqual((await result1).type, 'cancelled'); assertResult(await result2, 'test-0'); @@ -52,12 +52,12 @@ suite('CachedResponse', () => { const doc = await createTextDocument(); const response = new CachedResponse(); - const cancelledResponder = createEventualResponder(); + const cancelledResponder = createEventualResponder(); const result1 = response.execute(doc, respondWith('test-0')); const result2 = response.execute(doc, () => cancelledResponder.promise); const result3 = response.execute(doc, respondWith('test-1')); - cancelledResponder.resolve(new CancelledResponse('cancelled')); + cancelledResponder.resolve(new ServerResponse.Cancelled('cancelled')); assertResult(await result1, 'test-0'); assertResult(await result2, 'test-0'); @@ -69,8 +69,8 @@ suite('CachedResponse', () => { const doc2 = await createTextDocument(); const response = new CachedResponse(); - const cancelledResponder = createEventualResponder(); - const cancelledResponder2 = createEventualResponder(); + const cancelledResponder = createEventualResponder(); + const cancelledResponder2 = createEventualResponder(); const result1 = response.execute(doc1, () => cancelledResponder.promise); const result2 = response.execute(doc1, respondWith('test-0')); @@ -79,8 +79,8 @@ suite('CachedResponse', () => { const result5 = response.execute(doc2, respondWith('test-2')); const result6 = response.execute(doc1, respondWith('test-3')); - cancelledResponder.resolve(new CancelledResponse('cancelled')); - cancelledResponder2.resolve(new CancelledResponse('cancelled')); + cancelledResponder.resolve(new ServerResponse.Cancelled('cancelled')); + cancelledResponder2.resolve(new ServerResponse.Cancelled('cancelled')); assert.strictEqual((await result1).type, 'cancelled'); assertResult(await result2, 'test-0'); @@ -99,7 +99,7 @@ function createTextDocument() { return vscode.workspace.openTextDocument({ language: 'javascript', content: '' }); } -function assertResult(result: ServerResponse, command: string) { +function assertResult(result: ServerResponse.Response, command: string) { if (result.type === 'response') { assert.strictEqual(result.command, command); } else { diff --git a/extensions/typescript-language-features/src/tsServer/cachedResponse.ts b/extensions/typescript-language-features/src/tsServer/cachedResponse.ts index ea8414cf8a5..2fb9b542281 100644 --- a/extensions/typescript-language-features/src/tsServer/cachedResponse.ts +++ b/extensions/typescript-language-features/src/tsServer/cachedResponse.ts @@ -7,13 +7,13 @@ import * as vscode from 'vscode'; import * as Proto from '../protocol'; import { ServerResponse } from '../typescriptService'; -type Resolve = () => Promise>; +type Resolve = () => Promise>; /** * Caches a class of TS Server request based on document. */ export class CachedResponse { - private response?: Promise>; + private response?: Promise>; private version: number = -1; private document: string = ''; @@ -25,7 +25,7 @@ export class CachedResponse { public execute( document: vscode.TextDocument, resolve: Resolve - ): Promise> { + ): Promise> { if (this.response && this.matches(document)) { // Chain so that on cancellation we fall back to the next resolve return this.response = this.response.then(result => result.type === 'cancelled' ? resolve() : result); @@ -40,7 +40,7 @@ export class CachedResponse { private async reset( document: vscode.TextDocument, resolve: Resolve - ): Promise> { + ): Promise> { this.version = document.version; this.document = document.uri.toString(); return this.response = resolve(); diff --git a/extensions/typescript-language-features/src/tsServer/callbackMap.ts b/extensions/typescript-language-features/src/tsServer/callbackMap.ts index 897a587b924..7f96de57159 100644 --- a/extensions/typescript-language-features/src/tsServer/callbackMap.ts +++ b/extensions/typescript-language-features/src/tsServer/callbackMap.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as Proto from '../protocol'; -import { CancelledResponse, ServerResponse } from '../typescriptService'; +import { ServerResponse } from '../typescriptService'; export interface CallbackItem { readonly onSuccess: (value: R) => void; @@ -14,11 +14,11 @@ export interface CallbackItem { } export class CallbackMap { - private readonly _callbacks = new Map | undefined>>(); - private readonly _asyncCallbacks = new Map | undefined>>(); + private readonly _callbacks = new Map | undefined>>(); + private readonly _asyncCallbacks = new Map | undefined>>(); public destroy(cause: string): void { - const cancellation = new CancelledResponse(cause); + const cancellation = new ServerResponse.Cancelled(cause); for (const callback of this._callbacks.values()) { callback.onSuccess(cancellation); } @@ -29,7 +29,7 @@ export class CallbackMap { this._asyncCallbacks.clear(); } - public add(seq: number, callback: CallbackItem | undefined>, isAsync: boolean) { + public add(seq: number, callback: CallbackItem | undefined>, isAsync: boolean) { if (isAsync) { this._asyncCallbacks.set(seq, callback); } else { @@ -37,7 +37,7 @@ export class CallbackMap { } } - public fetch(seq: number): CallbackItem | undefined> | undefined { + public fetch(seq: number): CallbackItem | undefined> | undefined { const callback = this._callbacks.get(seq) || this._asyncCallbacks.get(seq); this.delete(seq); return callback; diff --git a/extensions/typescript-language-features/src/tsServer/server.ts b/extensions/typescript-language-features/src/tsServer/server.ts index b657449ee32..6c237aa538d 100644 --- a/extensions/typescript-language-features/src/tsServer/server.ts +++ b/extensions/typescript-language-features/src/tsServer/server.ts @@ -8,7 +8,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; import * as Proto from '../protocol'; -import { CancelledResponse, NoContentResponse, ServerResponse } from '../typescriptService'; +import { ServerResponse } from '../typescriptService'; import API from '../utils/api'; import { TsServerLogLevel, TypeScriptServiceConfiguration } from '../utils/configuration'; import { Disposable } from '../utils/dispose'; @@ -299,7 +299,7 @@ export class TypeScriptServer extends Disposable { } finally { const callback = this.fetchCallback(seq); if (callback) { - callback.onSuccess(new CancelledResponse(`Cancelled request ${seq} - ${command}`)); + callback.onSuccess(new ServerResponse.Cancelled(`Cancelled request ${seq} - ${command}`)); } } } @@ -315,15 +315,15 @@ export class TypeScriptServer extends Disposable { callback.onSuccess(response); } else if (response.message === 'No content available.') { // Special case where response itself is successful but there is not any data to return. - callback.onSuccess(NoContentResponse); + callback.onSuccess(ServerResponse.NoContent); } else { callback.onError(new TypeScriptServerError(this._version, response)); } } public executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined; - public executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise>; - public executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> | undefined { + public executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise>; + public executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> | undefined { const request = this._requestQueue.createRequest(command, args); const requestInfo: RequestItem = { request, @@ -331,9 +331,9 @@ export class TypeScriptServer extends Disposable { isAsync: executeInfo.isAsync, queueingType: getQueueingType(command, executeInfo.lowPriority) }; - let result: Promise> | undefined; + let result: Promise> | undefined; if (executeInfo.expectsResult) { - result = new Promise>((resolve, reject) => { + result = new Promise>((resolve, reject) => { this._callbacks.add(request.seq, { onSuccess: resolve, onError: reject, startTime: Date.now(), isAsync: executeInfo.isAsync }, executeInfo.isAsync); if (executeInfo.token) { diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index 8f004c0e5eb..14a4d895800 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -11,19 +11,21 @@ import { TypeScriptServiceConfiguration } from './utils/configuration'; import Logger from './utils/logger'; import { PluginManager } from './utils/plugins'; -export class CancelledResponse { - public readonly type: 'cancelled' = 'cancelled'; +export namespace ServerResponse { - constructor( - public readonly reason: string - ) { } + export class Cancelled { + public readonly type = 'cancelled'; + + constructor( + public readonly reason: string + ) { } + } + + export const NoContent = new class { readonly type = 'noContent'; }; + + export type Response = T | Cancelled | typeof NoContent; } -export const NoContentResponse = new class { readonly type = 'noContent'; }; -export const LanguageServiceDisabledContentResponse = new class { readonly type = 'languageServiceDisabled'; }; - -export type ServerResponse = T | CancelledResponse | typeof NoContentResponse | typeof LanguageServiceDisabledContentResponse; - export interface TypeScriptRequestTypes { 'applyCodeActionCommand': [Proto.ApplyCodeActionCommandRequestArgs, Proto.ApplyCodeActionCommandResponse]; 'completionEntryDetails': [Proto.CompletionDetailsRequestArgs, Proto.CompletionDetailsResponse]; @@ -103,7 +105,7 @@ export interface ITypeScriptServiceClient { args: TypeScriptRequestTypes[K][0], token: vscode.CancellationToken, lowPriority?: boolean - ): Promise>; + ): Promise>; executeWithoutWaitingForResponse(command: 'open', args: Proto.OpenRequestArgs): void; executeWithoutWaitingForResponse(command: 'close', args: Proto.FileRequestArgs): void; @@ -111,7 +113,7 @@ export interface ITypeScriptServiceClient { executeWithoutWaitingForResponse(command: 'compilerOptionsForInferredProjects', args: Proto.SetCompilerOptionsForInferredProjectsArgs): void; executeWithoutWaitingForResponse(command: 'reloadProjects', args: null): void; - executeAsync(command: 'geterr', args: Proto.GeterrRequestArgs, token: vscode.CancellationToken): Promise>; + executeAsync(command: 'geterr', args: Proto.GeterrRequestArgs, token: vscode.CancellationToken): Promise>; /** * Cancel on going geterr requests and re-queue them after `f` has been evaluated. diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 799f24864c1..134942e4696 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -11,7 +11,7 @@ import BufferSyncSupport from './features/bufferSyncSupport'; import { DiagnosticKind, DiagnosticsManager } from './features/diagnostics'; import * as Proto from './protocol'; import { TypeScriptServer, TypeScriptServerSpawner } from './tsServer/server'; -import { ITypeScriptServiceClient, ServerResponse, LanguageServiceDisabledContentResponse } from './typescriptService'; +import { ITypeScriptServiceClient, ServerResponse } from './typescriptService'; import API from './utils/api'; import { TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration'; import { Disposable } from './utils/dispose'; @@ -423,6 +423,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType private serviceStarted(resendModels: boolean): void { const configureOptions: Proto.ConfigureRequestArguments = { hostInfo: 'vscode', + preferences: { + providePrefixAndSuffixTextForRename: true, + allowRenameOfImportPath: true, + } }; this.executeWithoutWaitingForResponse('configure', configureOptions); this.setCompilerOptionsForInferredProjects(this._configuration); @@ -590,7 +594,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType return undefined; } - public execute(command: string, args: any, token: vscode.CancellationToken, lowPriority?: boolean): Promise> { + public execute(command: string, args: any, token: vscode.CancellationToken, lowPriority?: boolean): Promise> { return this.executeImpl(command, args, { isAsync: false, token, @@ -607,7 +611,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType }); } - public executeAsync(command: string, args: Proto.GeterrRequestArgs, token: vscode.CancellationToken): Promise> { + public executeAsync(command: string, args: Proto.GeterrRequestArgs, token: vscode.CancellationToken): Promise> { return this.executeImpl(command, args, { isAsync: true, token, @@ -616,28 +620,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType } private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined; - private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise>; - private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> | undefined { + private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise>; + private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> | undefined { const runningServerState = this.service(); - - if (!runningServerState.langaugeServiceEnabled) { - const nonSemanticCommands: string[] = [ - 'change', - 'close', - 'compilerOptionsForInferredProjects', - 'configure', - 'format', - 'formatonkey', - 'getOutliningSpans', - 'open', - 'projectInfo', - 'reloadProjects', - ]; - if (nonSemanticCommands.indexOf(command) === -1) { - return Promise.resolve(LanguageServiceDisabledContentResponse); - } - } - return runningServerState.server.executeImpl(command, args, executeInfo); } diff --git a/extensions/typescript-language-features/src/utils/managedFileContext.ts b/extensions/typescript-language-features/src/utils/managedFileContext.ts index 36cca539ec3..81acf9ad505 100644 --- a/extensions/typescript-language-features/src/utils/managedFileContext.ts +++ b/extensions/typescript-language-features/src/utils/managedFileContext.ts @@ -5,29 +5,25 @@ import * as vscode from 'vscode'; import { isSupportedLanguageMode } from './languageModeIds'; +import { Disposable } from './dispose'; /** * When clause context set when the current file is managed by vscode's built-in typescript extension. */ -export default class ManagedFileContextManager { +export default class ManagedFileContextManager extends Disposable { private static readonly contextName = 'typescript.isManagedFile'; private isInManagedFileContext: boolean = false; - private readonly onDidChangeActiveTextEditorSub: vscode.Disposable; - public constructor( private readonly normalizePath: (resource: vscode.Uri) => string | undefined ) { - this.onDidChangeActiveTextEditorSub = vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this); + super(); + vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, this._disposables); this.onDidChangeActiveTextEditor(vscode.window.activeTextEditor); } - public dispose() { - this.onDidChangeActiveTextEditorSub.dispose(); - } - private onDidChangeActiveTextEditor(editor?: vscode.TextEditor): any { if (editor) { const isManagedFile = isSupportedLanguageMode(editor.document) && this.normalizePath(editor.document.uri) !== null; diff --git a/extensions/typescript-language-features/src/utils/typingsStatus.ts b/extensions/typescript-language-features/src/utils/typingsStatus.ts index 78f303f0f39..066fda960df 100644 --- a/extensions/typescript-language-features/src/utils/typingsStatus.ts +++ b/extensions/typescript-language-features/src/utils/typingsStatus.ts @@ -6,18 +6,19 @@ import * as vscode from 'vscode'; import { loadMessageBundle } from 'vscode-nls'; import { ITypeScriptServiceClient } from '../typescriptService'; +import { Disposable } from './dispose'; const localize = loadMessageBundle(); const typingsInstallTimeout = 30 * 1000; -export default class TypingsStatus extends vscode.Disposable { +export default class TypingsStatus extends Disposable { private _acquiringTypings: { [eventId: string]: NodeJS.Timer } = Object.create({}); private _client: ITypeScriptServiceClient; private _subscriptions: vscode.Disposable[] = []; constructor(client: ITypeScriptServiceClient) { - super(() => this.dispose()); + super(); this._client = client; this._subscriptions.push( @@ -28,6 +29,7 @@ export default class TypingsStatus extends vscode.Disposable { } public dispose(): void { + super.dispose(); this._subscriptions.forEach(x => x.dispose()); for (const eventId of Object.keys(this._acquiringTypings)) { diff --git a/extensions/typescript-language-features/src/utils/versionStatus.ts b/extensions/typescript-language-features/src/utils/versionStatus.ts index 6d7940c9a8a..0b3e3412617 100644 --- a/extensions/typescript-language-features/src/utils/versionStatus.ts +++ b/extensions/typescript-language-features/src/utils/versionStatus.ts @@ -6,21 +6,17 @@ import * as vscode from 'vscode'; import * as languageModeIds from './languageModeIds'; import { TypeScriptVersion } from './versionProvider'; +import { Disposable } from './dispose'; -export default class VersionStatus { - private readonly _onChangeEditorSub: vscode.Disposable; +export default class VersionStatus extends Disposable { private readonly _versionBarEntry: vscode.StatusBarItem; constructor( private readonly _normalizePath: (resource: vscode.Uri) => string | undefined ) { - this._versionBarEntry = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 99 /* to the right of editor status (100) */); - this._onChangeEditorSub = vscode.window.onDidChangeActiveTextEditor(this.showHideStatus, this); - } - - public dispose() { - this._versionBarEntry.dispose(); - this._onChangeEditorSub.dispose(); + super(); + this._versionBarEntry = this._register(vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 99 /* to the right of editor status (100) */)); + vscode.window.onDidChangeActiveTextEditor(this.showHideStatus, this, this._disposables); } public onDidChangeTypeScriptVersion(version: TypeScriptVersion) { diff --git a/package.json b/package.json index 58456599d0d..4f78caf443e 100644 --- a/package.json +++ b/package.json @@ -49,11 +49,11 @@ "vscode-chokidar": "1.6.5", "vscode-debugprotocol": "1.33.0", "vscode-nsfw": "1.1.1", - "vscode-proxy-agent": "0.2.0", + "vscode-proxy-agent": "0.3.0", "vscode-ripgrep": "^1.2.5", "vscode-sqlite3": "4.0.7", "vscode-textmate": "^4.0.1", - "vscode-xterm": "3.10.0-beta11-reflow", + "vscode-xterm": "3.11.0-beta1", "winreg": "^1.2.4", "yauzl": "^2.9.1", "yazl": "^2.4.3" diff --git a/resources/linux/code.desktop b/resources/linux/code.desktop index f93060d565b..dbc7818cecf 100644 --- a/resources/linux/code.desktop +++ b/resources/linux/code.desktop @@ -5,7 +5,7 @@ GenericName=Text Editor Exec=/usr/share/@@NAME@@/@@NAME@@ --unity-launch %F Icon=@@ICON@@ Type=Application -StartupNotify=true +StartupNotify=false StartupWMClass=@@NAME_SHORT@@ Categories=Utility;TextEditor;Development;IDE; MimeType=text/plain;inode/directory; diff --git a/resources/linux/snap/snapUpdate.sh b/resources/linux/snap/snapUpdate.sh deleted file mode 100755 index 77569bfc16a..00000000000 --- a/resources/linux/snap/snapUpdate.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -sleep 2 -$SNAP_NAME \ No newline at end of file diff --git a/src/main.js b/src/main.js index 0b6c8e18249..5d6dcfca955 100644 --- a/src/main.js +++ b/src/main.js @@ -106,7 +106,10 @@ function onReady() { process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || ''; // Load main in AMD - require('./bootstrap-amd').load('vs/code/electron-main/main'); + perf.mark('willLoadMainBundle'); + require('./bootstrap-amd').load('vs/code/electron-main/main', () => { + perf.mark('didLoadMainBundle'); + }); }; // We recevied a valid nlsConfig from a user defined locale @@ -148,7 +151,6 @@ function onReady() { function configureCommandlineSwitches(cliArgs, nodeCachedDataDir) { // Force pre-Chrome-60 color profile handling (for https://github.com/Microsoft/vscode/issues/51791) - // TODO@Ben check if future versions of Electron still support this flag app.commandLine.appendSwitch('disable-features', 'ColorCorrectRendering'); // Support JS Flags diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index 1bf4732a4a0..1303af547f4 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -19,6 +19,7 @@ "./typings/require-monaco.d.ts", "typings/thenable.d.ts", "typings/es6-promise.d.ts", + "typings/lib.es2018.promise.d.ts", "typings/lib.array-ext.d.ts", "typings/lib.ie11_safe_es6.d.ts", "vs/css.d.ts", @@ -35,4 +36,4 @@ "exclude": [ "node_modules/*" ] -} \ No newline at end of file +} diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index b28472f8120..530d76501d5 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -171,6 +171,7 @@ "./vs/editor/contrib/comment/test/lineCommentCommand.test.ts", "./vs/editor/contrib/contextmenu/contextmenu.ts", "./vs/editor/contrib/cursorUndo/cursorUndo.ts", + "./vs/editor/contrib/documentSymbols/outlineModel.ts", "./vs/editor/contrib/dnd/dnd.ts", "./vs/editor/contrib/dnd/dragAndDropCommand.ts", "./vs/editor/contrib/find/findController.ts", @@ -513,14 +514,8 @@ "./vs/workbench/api/shared/editor.ts", "./vs/workbench/api/shared/tasks.ts", "./vs/workbench/browser/actions.ts", - "./vs/workbench/browser/actions/toggleActivityBarVisibility.ts", - "./vs/workbench/browser/actions/toggleCenteredLayout.ts", - "./vs/workbench/browser/actions/toggleEditorLayout.ts", - "./vs/workbench/browser/actions/toggleSidebarPosition.ts", - "./vs/workbench/browser/actions/toggleSidebarVisibility.ts", - "./vs/workbench/browser/actions/toggleStatusbarVisibility.ts", - "./vs/workbench/browser/actions/toggleTabsVisibility.ts", - "./vs/workbench/browser/actions/toggleZenMode.ts", + "./vs/workbench/browser/actions/layoutActions.ts", + "./vs/workbench/browser/actions/navigationActions.ts", "./vs/workbench/browser/actions/workspaceActions.ts", "./vs/workbench/browser/actions/workspaceCommands.ts", "./vs/workbench/browser/composite.ts", @@ -531,6 +526,8 @@ "./vs/workbench/browser/parts/editor/baseEditor.ts", "./vs/workbench/browser/parts/editor/binaryDiffEditor.ts", "./vs/workbench/browser/parts/editor/binaryEditor.ts", + "./vs/workbench/browser/parts/editor/breadcrumbs.ts", + "./vs/workbench/browser/parts/editor/breadcrumbsModel.ts", "./vs/workbench/browser/parts/editor/editor.ts", "./vs/workbench/browser/parts/editor/editorControl.ts", "./vs/workbench/browser/parts/editor/editorWidgets.ts", @@ -575,7 +572,9 @@ "./vs/workbench/common/theme.ts", "./vs/workbench/common/viewlet.ts", "./vs/workbench/common/views.ts", - "./vs/workbench/electron-browser/actions.ts", + "./vs/workbench/electron-browser/actions/helpActions", + "./vs/workbench/electron-browser/actions/developerActions", + "./vs/workbench/electron-browser/actions/windowActions", "./vs/workbench/electron-browser/resources.ts", "./vs/workbench/electron-browser/window.ts", "./vs/workbench/parts/backup/common/backupRestorer.ts", @@ -748,7 +747,8 @@ "./vs/workbench/parts/welcome/walkThrough/node/walkThroughContentProvider.ts", "./vs/workbench/parts/welcome/walkThrough/node/walkThroughInput.ts", "./vs/workbench/parts/welcome/walkThrough/node/walkThroughUtils.ts", - "./vs/workbench/services/actions/electron-browser/menusExtensionPoint.ts", + "./vs/workbench/api/common/menusExtensionPoint.ts", + "./vs/workbench/api/common/configurationExtensionPoint.ts", "./vs/workbench/services/activity/common/activity.ts", "./vs/workbench/services/backup/common/backup.ts", "./vs/workbench/services/backup/node/backupFileService.ts", @@ -757,7 +757,6 @@ "./vs/workbench/services/commands/common/commandService.ts", "./vs/workbench/services/commands/test/common/commandService.test.ts", "./vs/workbench/services/configuration/common/configuration.ts", - "./vs/workbench/services/configuration/common/configurationExtensionPoint.ts", "./vs/workbench/services/configuration/common/configurationModels.ts", "./vs/workbench/services/configuration/common/jsonEditing.ts", "./vs/workbench/services/configuration/node/jsonEditingService.ts", @@ -839,13 +838,6 @@ "./vs/workbench/services/search/common/searchHelpers.ts", "./vs/workbench/services/search/node/fileSearch.ts", "./vs/workbench/services/search/node/fileSearchManager.ts", - "./vs/workbench/services/search/node/legacy/rawLegacyTextSearchService.ts", - "./vs/workbench/services/search/node/legacy/search.ts", - "./vs/workbench/services/search/node/legacy/textSearch.ts", - "./vs/workbench/services/search/node/legacy/textSearchWorkerProvider.ts", - "./vs/workbench/services/search/node/legacy/worker/searchWorker.ts", - "./vs/workbench/services/search/node/legacy/worker/searchWorkerApp.ts", - "./vs/workbench/services/search/node/legacy/worker/searchWorkerIpc.ts", "./vs/workbench/services/search/node/rawSearchService.ts", "./vs/workbench/services/search/node/ripgrepFileSearch.ts", "./vs/workbench/services/search/node/ripgrepSearchProvider.ts", diff --git a/src/typings/electron.d.ts b/src/typings/electron.d.ts index 85ff31b320f..8f72a1bc565 100644 --- a/src/typings/electron.d.ts +++ b/src/typings/electron.d.ts @@ -1,4 +1,4 @@ -// Type definitions for Electron 3.1.0 +// Type definitions for Electron 3.1.2 // Project: http://electronjs.org/ // Definitions by: The Electron Team // Definitions: https://github.com/electron/electron-typescript-definitions @@ -2794,7 +2794,9 @@ declare namespace Electron { * registered shortcut is pressed by the user. When the accelerator is already * taken by other applications, this call will silently fail. This behavior is * intended by operating systems, since they don't want applications to fight for - * global shortcuts. + * global shortcuts. The following accelerators will not be registered successfully + * on macOS 10.14 Mojave unless the app has been authorized as a trusted + * accessibility client: */ register(accelerator: Accelerator, callback: Function): void; /** @@ -6307,14 +6309,14 @@ declare namespace Electron { * connection is made to the server, but before any http data is sent. The callback * has to be called with an response object. */ - onBeforeSendHeaders(filter: OnBeforeSendHeadersFilter, listener: Function): void; + onBeforeSendHeaders(filter: OnBeforeSendHeadersFilter, listener: (details: OnBeforeSendHeadersDetails, callback: (response: OnBeforeSendHeadersResponse) => void) => void): void; /** * The listener will be called with listener(details, callback) before sending an * HTTP request, once the request headers are available. This may occur after a TCP * connection is made to the server, but before any http data is sent. The callback * has to be called with an response object. */ - onBeforeSendHeaders(listener: Function): void; + onBeforeSendHeaders(listener: (details: OnBeforeSendHeadersDetails, callback: (response: OnBeforeSendHeadersResponse) => void) => void): void; /** * The listener will be called with listener(details) when a request is completed. */ @@ -6336,13 +6338,13 @@ declare namespace Electron { * headers of a request have been received. The callback has to be called with an * response object. */ - onHeadersReceived(filter: OnHeadersReceivedFilter, listener: Function): void; + onHeadersReceived(filter: OnHeadersReceivedFilter, listener: (details: OnHeadersReceivedDetails, callback: (response: OnHeadersReceivedResponse) => void) => void): void; /** * The listener will be called with listener(details, callback) when HTTP response * headers of a request have been received. The callback has to be called with an * response object. */ - onHeadersReceived(listener: Function): void; + onHeadersReceived(listener: (details: OnHeadersReceivedDetails, callback: (response: OnHeadersReceivedResponse) => void) => void): void; /** * The listener will be called with listener(details) when first byte of the * response body is received. For HTTP requests, this means that the status line @@ -8054,6 +8056,16 @@ declare namespace Electron { urls: string[]; } + interface OnBeforeSendHeadersDetails { + id: number; + url: string; + method: string; + webContentsId?: number; + resourceType: string; + timestamp: number; + requestHeaders: RequestHeaders; + } + interface OnBeforeSendHeadersFilter { /** * Array of URL patterns that will be used to filter out the requests that do not @@ -8062,6 +8074,14 @@ declare namespace Electron { urls: string[]; } + interface OnBeforeSendHeadersResponse { + cancel?: boolean; + /** + * When provided, request will be made with these headers. + */ + requestHeaders?: RequestHeaders; + } + interface OnCompletedDetails { id: number; url: string; @@ -8105,6 +8125,18 @@ declare namespace Electron { urls: string[]; } + interface OnHeadersReceivedDetails { + id: number; + url: string; + method: string; + webContentsId?: number; + resourceType: string; + timestamp: number; + statusLine: string; + statusCode: number; + responseHeaders: ResponseHeaders; + } + interface OnHeadersReceivedFilter { /** * Array of URL patterns that will be used to filter out the requests that do not @@ -8113,6 +8145,19 @@ declare namespace Electron { urls: string[]; } + interface OnHeadersReceivedResponse { + cancel: boolean; + /** + * When provided, the server is assumed to have responded with these headers. + */ + responseHeaders?: ResponseHeaders; + /** + * Should be provided when overriding responseHeaders to change header status + * otherwise original response header's status will be used. + */ + statusLine?: string; + } + interface OnResponseStartedDetails { id: number; url: string; diff --git a/src/typings/lib.es2018.promise.d.ts b/src/typings/lib.es2018.promise.d.ts new file mode 100644 index 00000000000..9f7b2d38cb2 --- /dev/null +++ b/src/typings/lib.es2018.promise.d.ts @@ -0,0 +1,27 @@ +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 + +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ + +/** + * Represents the completion of an asynchronous operation + */ +interface Promise { + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): Promise; +} diff --git a/src/vs/base/browser/ui/centered/centeredViewLayout.ts b/src/vs/base/browser/ui/centered/centeredViewLayout.ts index 1b1710bed0c..a776ba92a6d 100644 --- a/src/vs/base/browser/ui/centered/centeredViewLayout.ts +++ b/src/vs/base/browser/ui/centered/centeredViewLayout.ts @@ -40,7 +40,7 @@ function toSplitViewView(view: IView, getHeight: () => number): ISplitViewView { get maximumSize() { return view.maximumWidth; }, get minimumSize() { return view.minimumWidth; }, onDidChange: Event.map(view.onDidChange, e => e && e.width), - layout: size => view.layout(size, getHeight()) + layout: size => view.layout(size, getHeight(), Orientation.HORIZONTAL) }; } @@ -78,7 +78,7 @@ export class CenteredViewLayout { this.resizeMargins(); } } else { - this.view.layout(width, height); + this.view.layout(width, height, Orientation.HORIZONTAL); } this.didLayout = true; } diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index 336329a05e2..5bbe5d9c30f 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -8,7 +8,9 @@ import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { tail2 as tail, equals } from 'vs/base/common/arrays'; import { orthogonal, IView, GridView, Sizing as GridViewSizing, Box, IGridViewStyles } from './gridview'; -import { Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; +import { $ } from 'vs/base/browser/dom'; +import { LayoutPriority } from 'vs/base/browser/ui/splitview/splitview'; export { Orientation } from './gridview'; @@ -186,6 +188,7 @@ export interface IGridStyles extends IGridViewStyles { } export interface IGridOptions { styles?: IGridStyles; + proportionalLayout?: boolean; } export class Grid implements IDisposable { @@ -646,3 +649,62 @@ export function createSerializedGrid(gridDescriptor: GridDescriptor): ISerialize height: height || 1 }; } + +export class View implements IView { + + readonly element = $('.grid-view-view'); + + private visible = false; + private width: number | undefined; + private height: number | undefined; + private orientation: Orientation = Orientation.HORIZONTAL; + + get minimumWidth(): number { return this.visible ? this.view.minimumWidth : 0; } + get maximumWidth(): number { return this.visible ? this.view.maximumWidth : (this.orientation === Orientation.HORIZONTAL ? 0 : Number.POSITIVE_INFINITY); } + get minimumHeight(): number { return this.visible ? this.view.minimumHeight : 0; } + get maximumHeight(): number { return this.visible ? this.view.maximumHeight : (this.orientation === Orientation.VERTICAL ? 0 : Number.POSITIVE_INFINITY); } + + private onDidChangeVisibility = new Emitter<{ width: number; height: number; } | undefined>(); + readonly onDidChange: Event<{ width: number; height: number; } | undefined>; + + get priority(): LayoutPriority | undefined { return this.view.priority; } + get snapSize(): number | undefined { return this.visible ? this.view.snapSize : undefined; } + + constructor(private view: IView) { + this.show(); + this.onDidChange = Event.any(this.onDidChangeVisibility.event, Event.filter(view.onDidChange, () => this.visible)); + } + + show(): void { + if (this.visible) { + return; + } + + this.visible = true; + + this.element.appendChild(this.view.element); + this.onDidChangeVisibility.fire(typeof this.width === 'number' ? { width: this.width, height: this.height! } : undefined); + } + + hide(): void { + if (!this.visible) { + return; + } + + this.visible = false; + + this.element.removeChild(this.view.element); + this.onDidChangeVisibility.fire(undefined); + } + + layout(width: number, height: number, orientation: Orientation): void { + if (!this.visible) { + return; + } + + this.view.layout(width, height, orientation); + this.width = width; + this.height = height; + this.orientation = orientation; + } +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 73c042929c1..9011a578e51 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -24,7 +24,7 @@ export interface IView { readonly onDidChange: Event<{ width: number; height: number; } | undefined>; readonly priority?: LayoutPriority; readonly snapSize?: number; - layout(width: number, height: number): void; + layout(width: number, height: number, orientation: Orientation): void; } export function orthogonal(orientation: Orientation): Orientation { @@ -147,7 +147,7 @@ class BranchNode implements ISplitView, IDisposable { this._orthogonalSize = orthogonalSize; this.element = $('.monaco-grid-branch-node'); - this.splitview = new SplitView(this.element, { orientation, styles }); + this.splitview = new SplitView(this.element, { orientation, styles, proportionalLayout }); this.splitview.layout(size); const onDidSashReset = Event.map(this.splitview.onDidSashReset, i => [i]); @@ -414,7 +414,7 @@ class LeafNode implements ISplitView, IDisposable { ) { this._orthogonalSize = orthogonalSize; - this._onDidViewChange = Event.map(this.view.onDidChange, this.orientation === Orientation.HORIZONTAL ? e => e && e.width : e => e && e.height); + this._onDidViewChange = Event.map(this.view.onDidChange, e => e && (this.orientation === Orientation.VERTICAL ? e.width : e.height)); this.onDidChange = Event.any(this._onDidViewChange, this._onDidSetLinkedNode.event, this._onDidLinkedWidthNodeChange.event, this._onDidLinkedHeightNodeChange.event); } @@ -480,12 +480,12 @@ class LeafNode implements ISplitView, IDisposable { layout(size: number): void { this._size = size; - return this.view.layout(this.width, this.height); + return this.view.layout(this.width, this.height, orthogonal(this.orientation)); } orthogonalLayout(size: number): void { this._orthogonalSize = size; - return this.view.layout(this.width, this.height); + return this.view.layout(this.width, this.height, orthogonal(this.orientation)); } dispose(): void { } diff --git a/src/vs/base/browser/ui/list/list.css b/src/vs/base/browser/ui/list/list.css index 5c0cfcf3a85..666bdaa9167 100644 --- a/src/vs/base/browser/ui/list/list.css +++ b/src/vs/base/browser/ui/list/list.css @@ -29,6 +29,11 @@ height: 100%; } +.monaco-list.horizontal-scrolling .monaco-list-rows { + width: auto; + min-width: 100%; +} + .monaco-list-row { position: absolute; -moz-box-sizing: border-box; @@ -67,6 +72,7 @@ .monaco-list-type-filter { display: flex; + align-items: center; position: absolute; border-radius: 2px; padding: 0px 3px; diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index 89af3a81a5f..49c953f0124 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -62,6 +62,14 @@ export interface IIdentityProvider { getId(element: T): { toString(): string; }; } +export enum ListAriaRootRole { + /** default tree structure role */ + TREE = 'tree', + + /** role='tree' can interfere with screenreaders reading nested elements inside the tree row. Use FORM in that case. */ + FORM = 'form' +} + export interface IKeyboardNavigationLabelProvider { getKeyboardNavigationLabel(element: T): { toString(): string; }; mightProducePrintableCharacter?(event: IKeyboardEvent): boolean; diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index 51129e1bd97..1acd51e015f 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -190,8 +190,8 @@ export class PagedList implements IDisposable { return this.list.getSelection(); } - layout(height?: number): void { - this.list.layout(height); + layout(height?: number, width?: number): void { + this.list.layout(height, width); } reveal(index: number, relativeTop?: number): void { diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 701ab69ceb2..b599fe92406 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -21,7 +21,7 @@ import { memoize } from 'vs/base/common/decorators'; import { Range, IRange } from 'vs/base/common/range'; import { equals, distinct } from 'vs/base/common/arrays'; import { DataTransfers, StaticDND, IDragAndDropData } from 'vs/base/browser/dnd'; -import { disposableTimeout } from 'vs/base/common/async'; +import { disposableTimeout, Delayer } from 'vs/base/common/async'; interface IItem { readonly id: string; @@ -29,8 +29,9 @@ interface IItem { readonly templateId: string; row: IRow | null; size: number; + width: number | undefined; hasDynamicHeight: boolean; - renderWidth: number | undefined; + lastDynamicHeightWidth: number | undefined; uri: string | undefined; dropTarget: boolean; dragStartDisposable: IDisposable; @@ -47,6 +48,7 @@ export interface IListViewOptions { readonly setRowLineHeight?: boolean; readonly supportDynamicHeights?: boolean; readonly mouseSupport?: boolean; + readonly horizontalScrolling?: boolean; } const DefaultOptions = { @@ -60,7 +62,8 @@ const DefaultOptions = { onDragStart(): void { }, onDragOver() { return false; }, drop() { } - } + }, + horizontalScrolling: false }; export class ElementsDragAndDropData implements IDragAndDropData { @@ -154,12 +157,15 @@ export class ListView implements ISpliceable, IDisposable { private scrollableElement: ScrollableElement; private _scrollHeight: number; private scrollableElementUpdateDisposable: IDisposable | null = null; + private scrollableElementWidthDelayer = new Delayer(50); private splicing = false; private dragOverAnimationDisposable: IDisposable | undefined; private dragOverAnimationStopDisposable: IDisposable = Disposable.None; private dragOverMouseY: number; private setRowLineHeight: boolean; private supportDynamicHeights: boolean; + private horizontalScrolling: boolean; + private scrollWidth: number | undefined; private canUseTranslate3d: boolean | undefined = undefined; private dnd: IListViewDragAndDrop; @@ -175,6 +181,8 @@ export class ListView implements ISpliceable, IDisposable { readonly onDidChangeContentHeight: Event = Event.latch(this._onDidChangeContentHeight.event); get contentHeight(): number { return this.rangeMap.size; } + readonly onDidScroll: Event; + // private _onDragStart = new Emitter<{ element: T, uri: string, event: DragEvent }>(); // readonly onDragStart = this._onDragStart.event; @@ -189,6 +197,10 @@ export class ListView implements ISpliceable, IDisposable { renderers: IListRenderer[], options: IListViewOptions = DefaultOptions ) { + if (options.horizontalScrolling && options.supportDynamicHeights) { + throw new Error('Horizontal scrolling and dynamic heights not supported simultaneously'); + } + this.items = []; this.itemId = 0; this.rangeMap = new RangeMap(); @@ -206,13 +218,16 @@ export class ListView implements ISpliceable, IDisposable { this.domNode.className = 'monaco-list'; DOM.toggleClass(this.domNode, 'mouse-support', typeof options.mouseSupport === 'boolean' ? options.mouseSupport : true); + this.horizontalScrolling = getOrDefault(options, o => o.horizontalScrolling, DefaultOptions.horizontalScrolling); + DOM.toggleClass(this.domNode, 'horizontal-scrolling', this.horizontalScrolling); + this.rowsContainer = document.createElement('div'); this.rowsContainer.className = 'monaco-list-rows'; Gesture.addTarget(this.rowsContainer); this.scrollableElement = new ScrollableElement(this.rowsContainer, { alwaysConsumeMouseWheel: true, - horizontal: ScrollbarVisibility.Hidden, + horizontal: this.horizontalScrolling ? ScrollbarVisibility.Auto : ScrollbarVisibility.Hidden, vertical: getOrDefault(options, o => o.verticalScrollMode, DefaultOptions.verticalScrollMode), useShadows: getOrDefault(options, o => o.useShadows, DefaultOptions.useShadows) }); @@ -222,6 +237,7 @@ export class ListView implements ISpliceable, IDisposable { this.disposables = [this.rangeMap, this.gesture, this.scrollableElement, this.cache]; + this.onDidScroll = Event.signal(this.scrollableElement.onScroll); this.scrollableElement.onScroll(this.onScroll, this, this.disposables); domEvent(this.rowsContainer, TouchEventType.Change)(this.onTouchChange, this, this.disposables); @@ -275,8 +291,9 @@ export class ListView implements ISpliceable, IDisposable { element, templateId: this.virtualDelegate.getTemplateId(element), size: this.virtualDelegate.getHeight(element), + width: undefined, hasDynamicHeight: !!this.virtualDelegate.hasDynamicHeight && this.virtualDelegate.hasDynamicHeight(element), - renderWidth: undefined, + lastDynamicHeightWidth: undefined, row: null, uri: undefined, dropTarget: false, @@ -324,7 +341,7 @@ export class ListView implements ISpliceable, IDisposable { } } - this.updateScrollHeight(); + this.eventuallyUpdateScrollDimensions(); if (this.supportDynamicHeights) { this.rerender(this.scrollTop, this.renderHeight); @@ -333,18 +350,62 @@ export class ListView implements ISpliceable, IDisposable { return deleted.map(i => i.element); } - private updateScrollHeight(): void { + private eventuallyUpdateScrollDimensions(): void { this._scrollHeight = this.contentHeight; this.rowsContainer.style.height = `${this._scrollHeight}px`; if (!this.scrollableElementUpdateDisposable) { this.scrollableElementUpdateDisposable = DOM.scheduleAtNextAnimationFrame(() => { - this.scrollableElement.setScrollDimensions({ scrollHeight: this._scrollHeight }); + this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight }); + this.updateScrollWidth(); this.scrollableElementUpdateDisposable = null; }); } } + private eventuallyUpdateScrollWidth(): void { + if (!this.horizontalScrolling) { + return; + } + + this.scrollableElementWidthDelayer.trigger(() => this.updateScrollWidth()); + } + + private updateScrollWidth(): void { + if (!this.horizontalScrolling) { + return; + } + + if (this.items.length === 0) { + this.scrollableElement.setScrollDimensions({ scrollWidth: 0 }); + } + + let scrollWidth = 0; + + for (const item of this.items) { + if (typeof item.width !== 'undefined') { + scrollWidth = Math.max(scrollWidth, item.width); + } + } + + this.scrollWidth = scrollWidth; + this.scrollableElement.setScrollDimensions({ scrollWidth: scrollWidth + 10 }); + } + + updateWidth(index: number): void { + if (!this.horizontalScrolling || typeof this.scrollWidth === 'undefined') { + return; + } + + const item = this.items[index]; + this.measureItemWidth(item); + + if (typeof item.width !== 'undefined' && item.width > this.scrollWidth) { + this.scrollWidth = item.width; + this.scrollableElement.setScrollDimensions({ scrollWidth: this.scrollWidth + 10 }); + } + } + get length(): number { return this.items.length; } @@ -379,7 +440,7 @@ export class ListView implements ISpliceable, IDisposable { return this.rangeMap.indexAfter(position); } - layout(height?: number): void { + layout(height?: number, width?: number): void { let scrollDimensions: INewScrollDimensions = { height: height || DOM.getContentHeight(this.domNode) }; @@ -387,23 +448,29 @@ export class ListView implements ISpliceable, IDisposable { if (this.scrollableElementUpdateDisposable) { this.scrollableElementUpdateDisposable.dispose(); this.scrollableElementUpdateDisposable = null; - scrollDimensions.scrollHeight = this._scrollHeight; + scrollDimensions.scrollHeight = this.scrollHeight; } this.scrollableElement.setScrollDimensions(scrollDimensions); - } - layoutWidth(width: number): void { - this.renderWidth = width; + if (typeof width !== 'undefined') { + this.renderWidth = width; - if (this.supportDynamicHeights) { - this.rerender(this.scrollTop, this.renderHeight); + if (this.supportDynamicHeights) { + this.rerender(this.scrollTop, this.renderHeight); + } + + if (this.horizontalScrolling) { + this.scrollableElement.setScrollDimensions({ + width: width || DOM.getContentWidth(this.domNode) + }); + } } } // Render - private render(renderTop: number, renderHeight: number): void { + private render(renderTop: number, renderHeight: number, renderLeft: number, scrollWidth: number): void { const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); const renderRange = this.getRenderRange(renderTop, renderHeight); @@ -426,14 +493,16 @@ export class ListView implements ISpliceable, IDisposable { const canUseTranslate3d = !isWindows && !browser.isFirefox && browser.getZoomLevel() === 0; if (canUseTranslate3d) { - const transform = `translate3d(0px, -${renderTop}px, 0px)`; + const transform = `translate3d(-${renderLeft}px, -${renderTop}px, 0px)`; this.rowsContainer.style.transform = transform; this.rowsContainer.style.webkitTransform = transform; if (canUseTranslate3d !== this.canUseTranslate3d) { + this.rowsContainer.style.left = '0'; this.rowsContainer.style.top = '0'; } } else { + this.rowsContainer.style.left = `-${renderLeft}px`; this.rowsContainer.style.top = `-${renderTop}px`; if (canUseTranslate3d !== this.canUseTranslate3d) { @@ -442,7 +511,12 @@ export class ListView implements ISpliceable, IDisposable { } } + if (this.horizontalScrolling) { + this.rowsContainer.style.width = `${Math.max(scrollWidth, this.renderWidth)}px`; + } + this.canUseTranslate3d = canUseTranslate3d; + this.lastRenderTop = renderTop; this.lastRenderHeight = renderHeight; } @@ -467,6 +541,11 @@ export class ListView implements ISpliceable, IDisposable { this.updateItemInDOM(item, index); const renderer = this.renderers.get(item.templateId); + + if (!renderer) { + throw new Error(`No renderer found for template id ${item.templateId}`); + } + if (renderer) { renderer.renderElement(item.element, index, item.row.templateData); } @@ -479,6 +558,31 @@ export class ListView implements ISpliceable, IDisposable { const onDragStart = domEvent(item.row.domNode!, 'dragstart'); item.dragStartDisposable = onDragStart(event => this.onDragStart(item.element, uri, event)); } + + if (this.horizontalScrolling) { + this.measureItemWidth(item); + this.eventuallyUpdateScrollWidth(); + } + } + + private measureItemWidth(item: IItem): void { + if (!item.row || !item.row.domNode) { + return; + } + + item.row.domNode.style.width = 'fit-content'; + item.width = DOM.getContentWidth(item.row.domNode); + const style = window.getComputedStyle(item.row.domNode); + + if (style.paddingLeft) { + item.width += parseFloat(style.paddingLeft); + } + + if (style.paddingRight) { + item.width += parseFloat(style.paddingRight); + } + + item.row.domNode.style.width = ''; } private updateItemInDOM(item: IItem, index: number): void { @@ -507,6 +611,10 @@ export class ListView implements ISpliceable, IDisposable { this.cache.release(item.row!); item.row = null; + + if (this.horizontalScrolling) { + this.eventuallyUpdateScrollWidth(); + } } getScrollTop(): number { @@ -518,7 +626,7 @@ export class ListView implements ISpliceable, IDisposable { if (this.scrollableElementUpdateDisposable) { this.scrollableElementUpdateDisposable.dispose(); this.scrollableElementUpdateDisposable = null; - this.scrollableElement.setScrollDimensions({ scrollHeight: this._scrollHeight }); + this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight }); } this.scrollableElement.setScrollPosition({ scrollTop }); @@ -533,7 +641,7 @@ export class ListView implements ISpliceable, IDisposable { } get scrollHeight(): number { - return this._scrollHeight; + return this._scrollHeight + (this.horizontalScrolling ? 10 : 0); } // Events @@ -580,7 +688,7 @@ export class ListView implements ISpliceable, IDisposable { private onScroll(e: ScrollEvent): void { try { - this.render(e.scrollTop, e.height); + this.render(e.scrollTop, e.height, e.scrollLeft, e.scrollWidth); if (this.supportDynamicHeights) { this.rerender(e.scrollTop, e.height); @@ -879,7 +987,7 @@ export class ListView implements ISpliceable, IDisposable { if (!didChange) { if (heightDiff !== 0) { - this.updateScrollHeight(); + this.eventuallyUpdateScrollDimensions(); } const unrenderRanges = Range.relativeComplement(previousRenderRange, renderRange); @@ -922,7 +1030,7 @@ export class ListView implements ISpliceable, IDisposable { private probeDynamicHeight(index: number): number { const item = this.items[index]; - if (!item.hasDynamicHeight || item.renderWidth === this.renderWidth) { + if (!item.hasDynamicHeight || item.lastDynamicHeightWidth === this.renderWidth) { return 0; } @@ -936,7 +1044,7 @@ export class ListView implements ISpliceable, IDisposable { renderer.renderElement(item.element, index, row.templateData); } item.size = row.domNode!.offsetHeight; - item.renderWidth = this.renderWidth; + item.lastDynamicHeightWidth = this.renderWidth; this.rowsContainer.removeChild(row.domNode!); this.cache.release(row); diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 3620125f0aa..87da7ffcb11 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -16,7 +16,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Event, Emitter, EventBufferer } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; -import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction } from './list'; +import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, ListAriaRootRole } from './list'; import { ListView, IListViewOptions, IListViewDragAndDrop } from './listView'; import { Color } from 'vs/base/common/color'; import { mixin } from 'vs/base/common/objects'; @@ -723,11 +723,11 @@ export class DefaultStyleController implements IStyleController { } if (styles.listHoverBackground) { - content.push(`.monaco-list${suffix}:not(.drop-target) .monaco-list-row:hover { background-color: ${styles.listHoverBackground}; }`); + content.push(`.monaco-list${suffix}:not(.drop-target) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`); } if (styles.listHoverForeground) { - content.push(`.monaco-list${suffix} .monaco-list-row:hover { color: ${styles.listHoverForeground}; }`); + content.push(`.monaco-list${suffix} .monaco-list-row:hover:not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`); } if (styles.listSelectionOutline) { @@ -764,6 +764,10 @@ export class DefaultStyleController implements IStyleController { content.push(`.monaco-list-type-filter { border: 1px solid ${styles.listMatchesOutline}; }`); } + if (styles.listNoMatchesOutline) { + content.push(`.monaco-list-type-filter.no-matches { border: 1px solid ${styles.listNoMatchesOutline}; }`); + } + if (styles.listMatchesShadow) { content.push(`.monaco-list-type-filter { box-shadow: 1px 1px 1px ${styles.listMatchesShadow}; }`); } @@ -780,6 +784,7 @@ export interface IListOptions extends IListStyles { readonly dnd?: IListDragAndDrop; readonly enableKeyboardNavigation?: boolean; readonly keyboardNavigationLabelProvider?: IKeyboardNavigationLabelProvider; + readonly ariaRole?: ListAriaRootRole; readonly ariaLabel?: string; readonly keyboardSupport?: boolean; readonly multipleSelectionSupport?: boolean; @@ -794,6 +799,7 @@ export interface IListOptions extends IListStyles { readonly setRowLineHeight?: boolean; readonly supportDynamicHeights?: boolean; readonly mouseSupport?: boolean; + readonly horizontalScrolling?: boolean; } export interface IListStyles { @@ -815,6 +821,7 @@ export interface IListStyles { listHoverOutline?: Color; listMatchesBackground?: Color; listMatchesOutline?: Color; + listNoMatchesOutline?: Color; listMatchesShadow?: Color; } @@ -838,7 +845,8 @@ const DefaultOptions = { onDragStart(): void { }, onDragOver() { return false; }, drop() { } - } + }, + ariaRootRole: ListAriaRootRole.TREE }; // TODO@Joao: move these utils into a SortedArray class @@ -1074,6 +1082,7 @@ export class List implements ISpliceable, IDisposable { return Event.map(this._onPin.event, indexes => this.toListEvent({ indexes })); } + get onDidScroll(): Event { return this.view.onDidScroll; } get onMouseClick(): Event> { return this.view.onMouseClick; } get onMouseDblClick(): Event> { return this.view.onMouseDblClick; } get onMouseMiddleClick(): Event> { return this.view.onMouseMiddleClick; } @@ -1158,7 +1167,13 @@ export class List implements ISpliceable, IDisposable { }; this.view = new ListView(container, virtualDelegate, renderers, viewOptions); - this.view.domNode.setAttribute('role', 'tree'); + + if (typeof _options.ariaRole !== 'string') { + this.view.domNode.setAttribute('role', ListAriaRootRole.TREE); + } else { + this.view.domNode.setAttribute('role', _options.ariaRole); + } + DOM.addClass(this.view.domNode, this.idPrefix); this.view.domNode.tabIndex = 0; @@ -1228,6 +1243,10 @@ export class List implements ISpliceable, IDisposable { this.eventBufferer.bufferEvents(() => this.spliceable.splice(start, deleteCount, elements)); } + updateWidth(index: number): void { + this.view.updateWidth(index); + } + element(index: number): T { return this.view.element(index); } @@ -1264,12 +1283,8 @@ export class List implements ISpliceable, IDisposable { this.view.domNode.focus(); } - layout(height?: number): void { - this.view.layout(height); - } - - layoutWidth(width: number): void { - this.view.layoutWidth(width); + layout(height?: number, width?: number): void { + this.view.layout(height, width); } setSelection(indexes: number[], browserEvent?: UIEvent): void { diff --git a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.css b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.css index f4744af5bb4..d24cc0b5258 100644 --- a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.css +++ b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.css @@ -1,7 +1,7 @@ @font-face { font-family: "octicons"; - src: url("./octicons.ttf?7b4c09277b7efac707b034fae8e52ed0") format("truetype"), -url("./octicons.svg?7b4c09277b7efac707b034fae8e52ed0#octicons") format("svg"); + src: url("./octicons.ttf?4cd2299755e93a2430ba5703f4476584") format("truetype"), +url("./octicons.svg?4cd2299755e93a2430ba5703f4476584#octicons") format("svg"); } .octicon, .mega-octicon { @@ -240,3 +240,4 @@ url("./octicons.svg?7b4c09277b7efac707b034fae8e52ed0#octicons") format("svg"); .octicon-fold-up:before { content: "\f105" } .octicon-github-action:before { content: "\f106" } .octicon-play:before { content: "\f107" } +.octicon-request-changes:before { content: "\f108" } diff --git a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.svg b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.svg index 4309475d790..4581795255c 100644 --- a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.svg +++ b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.svg @@ -445,6 +445,9 @@ + diff --git a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.ttf b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.ttf index 747e34216ca..12b561cd47a 100644 Binary files a/src/vs/base/browser/ui/octiconLabel/octicons/octicons.ttf and b/src/vs/base/browser/ui/octiconLabel/octicons/octicons.ttf differ diff --git a/src/vs/base/browser/ui/splitview/panelview.ts b/src/vs/base/browser/ui/splitview/panelview.ts index a78bb77de5e..18fecbbaafa 100644 --- a/src/vs/base/browser/ui/splitview/panelview.ts +++ b/src/vs/base/browser/ui/splitview/panelview.ts @@ -109,6 +109,8 @@ export abstract class Panel implements IView { return headerSize + maximumBodySize; } + width: number; + constructor(options: IPanelOptions = {}) { this._expanded = typeof options.expanded === 'undefined' ? true : !!options.expanded; this.ariaHeaderLabel = options.ariaHeaderLabel || ''; @@ -188,12 +190,12 @@ export abstract class Panel implements IView { this.renderBody(body); } - layout(size: number): void { + layout(height: number): void { const headerSize = this.headerVisible ? Panel.HEADER_SIZE : 0; if (this.isExpanded()) { - this.layoutBody(size - headerSize); - this.expandedSize = size; + this.layoutBody(height - headerSize, this.width); + this.expandedSize = height; } } @@ -224,7 +226,7 @@ export abstract class Panel implements IView { protected abstract renderHeader(container: HTMLElement): void; protected abstract renderBody(container: HTMLElement): void; - protected abstract layoutBody(size: number): void; + protected abstract layoutBody(height: number, width: number): void; dispose(): void { this.disposables = dispose(this.disposables); @@ -369,6 +371,7 @@ export class PanelView extends Disposable { private dndContext: IDndContext = { draggable: null }; private el: HTMLElement; private panelItems: IPanelItem[] = []; + private width: number; private splitview: SplitView; private animationTimer: number | null = null; @@ -398,6 +401,7 @@ export class PanelView extends Disposable { const panelItem = { panel, disposable: combinedDisposable(disposables) }; this.panelItems.splice(index, 0, panelItem); + panel.width = this.width; this.splitview.addView(panel, size, index); if (this.dnd) { @@ -453,8 +457,14 @@ export class PanelView extends Disposable { return this.splitview.getViewSize(index); } - layout(size: number): void { - this.splitview.layout(size); + layout(height: number, width: number): void { + this.width = width; + + for (const panelItem of this.panelItems) { + panelItem.panel.width = width; + } + + this.splitview.layout(height); } private setupAnimation(): void { diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 85da0ba4eb9..921d5101b44 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -21,6 +21,7 @@ import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters'; import { getVisibleState, isFilterResult } from 'vs/base/browser/ui/tree/indexTreeModel'; import { localize } from 'vs/nls'; import { disposableTimeout } from 'vs/base/common/async'; +import { isMacintosh } from 'vs/base/common/platform'; function asTreeDragAndDropData(data: IDragAndDropData): IDragAndDropData { if (data instanceof ElementsDragAndDropData) { @@ -179,6 +180,8 @@ interface ITreeRendererOptions { class TreeRenderer implements IListRenderer, ITreeListTemplateData> { + private static DefaultIndent = 8; + readonly templateId: string; private renderedElements = new Map>(); private renderedNodes = new Map, ITreeListTemplateData>(); @@ -201,7 +204,7 @@ class TreeRenderer implements IListRenderer { templateData.twistie.style.marginLeft = `${node.depth * this.indent}px`; @@ -221,7 +224,8 @@ class TreeRenderer implements IListRenderer implements IListRenderer implements ITreeFilter { +class TypeFilter implements ITreeFilter, IDisposable { + + private _totalCount = 0; + get totalCount(): number { return this._totalCount; } + private _matchCount = 0; + get matchCount(): number { return this._matchCount; } private _pattern: string; private _lowercasePattern: string; + private disposables: IDisposable[] = []; set pattern(pattern: string) { this._pattern = pattern; @@ -289,7 +299,9 @@ class TypeFilter implements ITreeFilter { private tree: AbstractTree, private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider, private _filter?: ITreeFilter - ) { } + ) { + tree.onWillRefilter(this.reset, this, this.disposables); + } filter(element: T, parentVisibility: TreeVisibility): TreeFilterResult { if (this._filter) { @@ -314,7 +326,10 @@ class TypeFilter implements ITreeFilter { } } + this._totalCount++; + if (this.tree.options.simpleKeyboardNavigation || !this._pattern) { + this._matchCount++; return { data: FuzzyScore.Default, visibility: true }; } @@ -332,8 +347,18 @@ class TypeFilter implements ITreeFilter { // return parentVisibility === TreeVisibility.Visible ? true : TreeVisibility.Recurse; } + this._matchCount++; return { data: score, visibility: true }; } + + private reset(): void { + this._totalCount = 0; + this._matchCount = 0; + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } } class TypeFilterController implements IDisposable { @@ -356,6 +381,7 @@ class TypeFilterController implements IDisposable { constructor( private tree: AbstractTree, + model: ITreeModel, private view: List>, private filter: TypeFilter, private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider @@ -380,6 +406,8 @@ class TypeFilterController implements IDisposable { this.clearDomNode.tabIndex = -1; this.clearDomNode.title = localize('clear', "Clear"); + model.onDidSplice(this.onDidSpliceModel, this, this.disposables); + tree.onDidUpdateOptions(this.onDidUpdateTreeOptions, this, this.disposables); this.onDidUpdateTreeOptions(tree.options); } @@ -393,7 +421,7 @@ class TypeFilterController implements IDisposable { this.filterOnTypeDomNode.checked = !!options.filterOnType; this.tree.refilter(); - this.updateMessage(); + this.render(); } private enable(): void { @@ -405,14 +433,14 @@ class TypeFilterController implements IDisposable { const onKeyDown = Event.chain(domEvent(this.view.getHTMLElement(), 'keydown')) .filter(e => !isInputElement(e.target as HTMLElement) || e.target === this.filterOnTypeDomNode) .map(e => new StandardKeyboardEvent(e)) - .filter(e => e.keyCode === KeyCode.Backspace || e.keyCode === KeyCode.Escape || isPrintableCharEvent(e)) + .filter(e => isPrintableCharEvent(e) || (this._pattern.length > 0 && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? e.altKey : e.ctrlKey)))) .forEach(e => { e.stopPropagation(); e.preventDefault(); }) .event; const onClear = domEvent(this.clearDomNode, 'click'); const onInput = Event.chain(Event.any(onKeyDown, onClear)) .reduce((previous: string, e) => { - if (e instanceof MouseEvent || e.keyCode === KeyCode.Escape) { + if (e instanceof MouseEvent || e.keyCode === KeyCode.Escape || (e.keyCode === KeyCode.Backspace && (isMacintosh ? e.altKey : e.ctrlKey))) { return ''; } @@ -427,7 +455,7 @@ class TypeFilterController implements IDisposable { onInput(this.onInput, this, this.enabledDisposables); this.filter.pattern = ''; this.tree.refilter(); - this.updateMessage(); + this.render(); this.enabled = true; } @@ -439,7 +467,7 @@ class TypeFilterController implements IDisposable { this.domNode.remove(); this.enabledDisposables = dispose(this.enabledDisposables); this.tree.refilter(); - this.updateMessage(); + this.render(); this.enabled = false; } @@ -453,16 +481,11 @@ class TypeFilterController implements IDisposable { this.tree.domFocus(); } - this.labelDomNode.textContent = pattern.length > 16 - ? '…' + pattern.substr(pattern.length - 16) - : pattern; - this._pattern = pattern; this.filter.pattern = pattern; this.tree.refilter(); this.tree.focusNext(0, true); - - this.updateMessage(); + this.render(); } private onDragStart(): void { @@ -489,6 +512,9 @@ class TypeFilterController implements IDisposable { const onDragOver = (event: DragEvent) => { const x = event.screenX - left; + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'none'; + } if (x < midContainerWidth) { positionClassName = 'nw'; @@ -521,11 +547,20 @@ class TypeFilterController implements IDisposable { disposables.push(toDisposable(() => StaticDND.CurrentDragAndDropData = undefined)); } + private onDidSpliceModel(): void { + if (!this.enabled || this.pattern.length === 0) { + return; + } + + this.tree.refilter(); + this.render(); + } + private onDidChangeFilterOnType(): void { this.tree.updateOptions({ filterOnType: this.filterOnTypeDomNode.checked }); this.tree.refilter(); this.tree.domFocus(); - this.updateMessage(); + this.render(); this.updateFilterOnTypeTitle(); } @@ -537,12 +572,18 @@ class TypeFilterController implements IDisposable { } } - private updateMessage(): void { - if (this.pattern && this.view.length === 0) { + private render(): void { + const noMatches = this.filter.totalCount > 0 && this.filter.matchCount === 0; + + if (this.pattern && this.tree.options.filterOnType && noMatches) { this.messageDomNode.textContent = localize('empty', "No elements found"); } else { this.messageDomNode.innerHTML = ''; } + + toggleClass(this.domNode, 'no-matches', noMatches); + this.domNode.title = localize('found', "Matched {0} out of {1} elements", this.filter.matchCount, this.filter.totalCount); + this.labelDomNode.textContent = this.pattern.length > 16 ? '…' + this.pattern.substr(this.pattern.length - 16) : this.pattern; } dispose() { @@ -600,6 +641,8 @@ export abstract class AbstractTree implements IDisposable private _onDidUpdateOptions = new Emitter>(); readonly onDidUpdateOptions = this._onDidUpdateOptions.event; + get onDidScroll(): Event { return this.view.onDidScroll; } + get onDidChangeFocus(): Event> { return Event.map(this.view.onFocusChange, asTreeEvent); } get onDidChangeSelection(): Event> { return Event.map(this.view.onSelectionChange, asTreeEvent); } get onDidOpen(): Event> { return Event.map(this.view.onDidOpen, asTreeEvent); } @@ -618,6 +661,9 @@ export abstract class AbstractTree implements IDisposable get onDidChangeCollapseState(): Event> { return this.model.onDidChangeCollapseState; } get onDidChangeRenderNodeCount(): Event> { return this.model.onDidChangeRenderNodeCount; } + private _onWillRefilter = new Emitter(); + readonly onWillRefilter: Event = this._onWillRefilter.event; + get onDidDispose(): Event { return this.view.onDidDispose; } constructor( @@ -632,11 +678,12 @@ export abstract class AbstractTree implements IDisposable this.renderers = renderers.map(r => new TreeRenderer(r, onDidChangeCollapseStateRelay.event, _options)); this.disposables.push(...this.renderers); - let filter: ITreeFilter | undefined; + let filter: TypeFilter | undefined; if (_options.keyboardNavigationLabelProvider) { - filter = new TypeFilter(this, _options.keyboardNavigationLabelProvider, _options.filter as ITreeFilter) as ITreeFilter; // TODO need typescript help here - _options = { ..._options, filter }; + filter = new TypeFilter(this, _options.keyboardNavigationLabelProvider, _options.filter as ITreeFilter); + _options = { ..._options, filter: filter as ITreeFilter }; // TODO need typescript help here + this.disposables.push(filter); } this.view = new List(container, treeDelegate, this.renderers, asListOptions(() => this.model, _options)); @@ -657,8 +704,18 @@ export abstract class AbstractTree implements IDisposable } if (_options.keyboardNavigationLabelProvider) { - const typeFilterController = new TypeFilterController(this, this.view, filter as TypeFilter, _options.keyboardNavigationLabelProvider); - this.focusNavigationFilter = node => !typeFilterController.pattern || !FuzzyScore.isDefault(node.filterData as any as FuzzyScore); // TODO@joao + const typeFilterController = new TypeFilterController(this, this.model, this.view, filter!, _options.keyboardNavigationLabelProvider); + this.focusNavigationFilter = node => { + if (!typeFilterController.pattern) { + return true; + } + + if (filter!.totalCount > 0 && filter!.matchCount === 0) { + return true; + } + + return !FuzzyScore.isDefault(node.filterData as any as FuzzyScore); + }; this.disposables.push(typeFilterController); } } @@ -678,6 +735,16 @@ export abstract class AbstractTree implements IDisposable return this._options; } + updateWidth(element: TRef): void { + const index = this.model.getListIndex(element); + + if (index === -1) { + return; + } + + this.view.updateWidth(index); + } + // Widget getHTMLElement(): HTMLElement { @@ -716,12 +783,8 @@ export abstract class AbstractTree implements IDisposable return this.getHTMLElement() === document.activeElement; } - layout(height?: number): void { - this.view.layout(height); - } - - layoutWidth(width: number): void { - this.view.layoutWidth(width); + layout(height?: number, width?: number): void { + this.view.layout(height, width); } style(styles: IListStyles): void { @@ -773,6 +836,7 @@ export abstract class AbstractTree implements IDisposable } refilter(): void { + this._onWillRefilter.fire(undefined); this.model.refilter(); } @@ -1002,4 +1066,4 @@ class TreeNavigator, TFilterData, TRef> implements IT this.index = this.view.length - 1; return this.current(); } -} \ No newline at end of file +} diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 4f308bf86f5..c4ea62fed4a 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -28,8 +28,8 @@ enum AsyncDataTreeNodeState { interface IAsyncDataTreeNode { element: TInput | T; readonly parent: IAsyncDataTreeNode | null; + readonly children: IAsyncDataTreeNode[]; readonly id?: string | null; - readonly children?: IAsyncDataTreeNode[]; state: AsyncDataTreeNodeState; } @@ -224,7 +224,7 @@ function asObjectTreeOptions(options?: IAsyncDataTreeOpt function asTreeElement(node: IAsyncDataTreeNode): ITreeElement> { return { element: node, - children: Iterator.map(Iterator.fromArray(node.children!), asTreeElement) + children: Iterator.map(Iterator.fromArray(node.children), asTreeElement) }; } @@ -255,7 +255,6 @@ export class AsyncDataTree implements IDisposable private readonly nodes = new Map>(); private readonly currentRefreshCalls = new Map, Promise>(); - // TODO@joao remove this private readonly refreshPromises = new Map, CancelablePromise>(); private readonly identityProvider?: IIdentityProvider; private readonly autoExpandSingleChildren: boolean; @@ -264,6 +263,8 @@ export class AsyncDataTree implements IDisposable protected readonly disposables: IDisposable[] = []; + get onDidScroll(): Event { return this.tree.onDidScroll; } + get onDidChangeFocus(): Event> { return Event.map(this.tree.onDidChangeFocus, asTreeEvent); } get onDidChangeSelection(): Event> { return Event.map(this.tree.onDidChangeSelection, asTreeEvent); } get onDidOpen(): Event> { return Event.map(this.tree.onDidOpen, asTreeEvent); } @@ -295,14 +296,14 @@ export class AsyncDataTree implements IDisposable this.root = { element: undefined!, parent: null, + children: [], state: AsyncDataTreeNodeState.Uninitialized, }; if (this.identityProvider) { this.root = { ...this.root, - id: null, - children: [], + id: null }; } @@ -349,8 +350,8 @@ export class AsyncDataTree implements IDisposable this.tree.domFocus(); } - layout(height?: number): void { - this.tree.layout(height); + layout(height?: number, width?: number): void { + this.tree.layout(height, width); } style(styles: IListStyles): void { @@ -394,6 +395,11 @@ export class AsyncDataTree implements IDisposable this.tree.refresh(node); } + updateWidth(element: T): void { + const node = this.getDataNode(element); + this.tree.updateWidth(node); + } + // Tree getNode(element: TInput | T = this.root.element): ITreeNode { @@ -538,7 +544,7 @@ export class AsyncDataTree implements IDisposable private async refreshNode(node: IAsyncDataTreeNode, recursive: boolean, reason: ChildrenResolutionReason, viewStateContext?: IAsyncDataTreeViewStateContext): Promise { await this.queueRefresh(node, recursive, reason, viewStateContext); - if (recursive && node.children) { + if (recursive) { await Promise.all(node.children.map(child => this.refreshNode(child, recursive, reason, viewStateContext))); } } @@ -651,7 +657,7 @@ export class AsyncDataTree implements IDisposable if (this.identityProvider) { nodeChildren = new Map(); - for (const child of node.children!) { + for (const child of node.children) { nodeChildren.set(child.id!, child); } } @@ -662,6 +668,7 @@ export class AsyncDataTree implements IDisposable element: { element, parent: node, + children: [], state: AsyncDataTreeNodeState.Uninitialized }, collapsible: !!this.dataSource.hasChildren(element), @@ -707,7 +714,7 @@ export class AsyncDataTree implements IDisposable asyncDataTreeNode.state = AsyncDataTreeNodeState.Uninitialized; if (this.tree.isCollapsed(asyncDataTreeNode === this.root ? null : asyncDataTreeNode)) { - asyncDataTreeNode.children!.length = 0; + asyncDataTreeNode.children.length = 0; return { element: asyncDataTreeNode, @@ -720,7 +727,7 @@ export class AsyncDataTree implements IDisposable let children: Iterator>> | undefined = undefined; if (collapsible) { - children = Iterator.map(Iterator.fromArray(asyncDataTreeNode.children!), asTreeElement); + children = Iterator.map(Iterator.fromArray(asyncDataTreeNode.children), asTreeElement); } return { @@ -749,10 +756,10 @@ export class AsyncDataTree implements IDisposable } }; - this.tree.setChildren(node === this.root ? null : node, children, onDidCreateNode, onDidDeleteNode); - - if (this.identityProvider) { - node.children!.splice(0, node.children!.length, ...children.map(c => c.element)); + // perf: if the node was and still is a leaf, avoid all these expensive no-ops + if (node.children.length > 0 || children.length > 0) { + this.tree.setChildren(node === this.root ? null : node, children, onDidCreateNode, onDidDeleteNode); + node.children.splice(0, node.children.length, ...children.map(c => c.element)); } } diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index 0b4fad00e98..3df971f5b55 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -61,6 +61,9 @@ export class IndexTreeModel, TFilterData = voi private filter?: ITreeFilter; private autoExpandSingleChildren: boolean; + private _onDidSplice = new Emitter(); + readonly onDidSplice = this._onDidSplice.event; + constructor(private list: ISpliceable>, rootElement: T, options: IIndexTreeModelOptions = {}) { this.collapseByDefault = typeof options.collapseByDefault === 'undefined' ? false : options.collapseByDefault; this.filter = options.filter; @@ -123,7 +126,9 @@ export class IndexTreeModel, TFilterData = voi deletedNodes.forEach(visit); } - return Iterator.map(Iterator.fromArray(deletedNodes), treeNodeToElement); + const result = Iterator.map(Iterator.fromArray(deletedNodes), treeNodeToElement); + this._onDidSplice.fire(undefined); + return result; } refresh(location: number[]): void { @@ -139,7 +144,8 @@ export class IndexTreeModel, TFilterData = voi } getListIndex(location: number[]): number { - return this.getTreeNodeWithListIndex(location).listIndex; + const { listIndex, visible } = this.getTreeNodeWithListIndex(location); + return visible ? listIndex : -1; } getListRenderCount(location: number[]): number { @@ -418,12 +424,12 @@ export class IndexTreeModel, TFilterData = voi } // expensive - private getTreeNodeWithListIndex(location: number[]): { node: IMutableTreeNode, listIndex: number, revealed: boolean } { + private getTreeNodeWithListIndex(location: number[]): { node: IMutableTreeNode, listIndex: number, revealed: boolean, visible: boolean } { if (location.length === 0) { - return { node: this.root, listIndex: -1, revealed: true }; + return { node: this.root, listIndex: -1, revealed: true, visible: false }; } - const { parentNode, listIndex, revealed } = this.getParentNodeWithListIndex(location); + const { parentNode, listIndex, revealed, visible } = this.getParentNodeWithListIndex(location); const index = location[location.length - 1]; if (index < 0 || index > parentNode.children.length) { @@ -432,10 +438,10 @@ export class IndexTreeModel, TFilterData = voi const node = parentNode.children[index]; - return { node, listIndex, revealed }; + return { node, listIndex, revealed, visible: visible && node.visible }; } - private getParentNodeWithListIndex(location: number[], node: IMutableTreeNode = this.root, listIndex: number = 0, revealed = true): { parentNode: IMutableTreeNode; listIndex: number; revealed: boolean; } { + private getParentNodeWithListIndex(location: number[], node: IMutableTreeNode = this.root, listIndex: number = 0, revealed = true, visible = true): { parentNode: IMutableTreeNode; listIndex: number; revealed: boolean; visible: boolean; } { const [index, ...rest] = location; if (index < 0 || index > node.children.length) { @@ -448,12 +454,13 @@ export class IndexTreeModel, TFilterData = voi } revealed = revealed && !node.collapsed; + visible = visible && node.visible; if (rest.length === 0) { - return { parentNode: node, listIndex, revealed }; + return { parentNode: node, listIndex, revealed, visible }; } - return this.getParentNodeWithListIndex(rest, node.children[index], listIndex + 1, revealed); + return this.getParentNodeWithListIndex(rest, node.children[index], listIndex + 1, revealed, visible); } getNode(location: number[] = []): ITreeNode { diff --git a/src/vs/base/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index e024cf1dd92..467a7c76e2f 100644 --- a/src/vs/base/browser/ui/tree/objectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/objectTreeModel.ts @@ -21,6 +21,7 @@ export class ObjectTreeModel, TFilterData extends Non private nodes = new Map>(); private sorter?: ITreeSorter>; + readonly onDidSplice: Event; readonly onDidChangeCollapseState: Event>; readonly onDidChangeRenderNodeCount: Event>; @@ -28,6 +29,7 @@ export class ObjectTreeModel, TFilterData extends Non constructor(list: ISpliceable>, options: IObjectTreeModelOptions = {}) { this.model = new IndexTreeModel(list, null, options); + this.onDidSplice = this.model.onDidSplice; this.onDidChangeCollapseState = this.model.onDidChangeCollapseState as Event>; this.onDidChangeRenderNodeCount = this.model.onDidChangeRenderNodeCount as Event>; diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 8fc05270f02..7cf94dbb023 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -97,6 +97,8 @@ export interface ICollapseStateChangeEvent { export interface ITreeModel { readonly rootRef: TRef; + + readonly onDidSplice: Event; readonly onDidChangeCollapseState: Event>; readonly onDidChangeRenderNodeCount: Event>; diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 807898c6989..79c7a1c8071 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -44,6 +44,9 @@ export function createCancelablePromise(callback: (token: CancellationToken) catch(reject?: ((reason: any) => TResult | Promise) | undefined | null): Promise { return this.then(undefined, reject); } + finally(onfinally?: (() => void) | undefined | null): Promise { + return always(promise, onfinally || (() => { })); + } }; } diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index bb69923e2e7..e7674b93648 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -378,16 +378,13 @@ export function createMatches(score: undefined | FuzzyScore): IMatch[] { return []; } - const [, matches, wordStart] = score; + const matches = score[1].toString(2); + const wordStart = score[2]; const res: IMatch[] = []; - for (let pos = wordStart; pos < _masks.length; pos++) { - const mask = _masks[pos]; - if (mask > matches) { - break; - } else if (matches & mask) { + for (let pos = wordStart; pos < _maxLen; pos++) { + if (matches[matches.length - (pos + 1)] === '1') { const last = res[res.length - 1]; - if (last && last.end === pos) { last.end = pos + 1; } else { @@ -400,14 +397,6 @@ export function createMatches(score: undefined | FuzzyScore): IMatch[] { const _maxLen = 53; -const _masks = (function () { - const result: number[] = []; - for (let pos = 0; pos < _maxLen; pos++) { - result.push(2 ** pos); - } - return result; -}()); - function initTable() { const table: number[][] = []; const row: number[] = [0]; diff --git a/src/vs/base/common/paths.ts b/src/vs/base/common/paths.ts index 26f885e6b7b..33496d1d7d9 100644 --- a/src/vs/base/common/paths.ts +++ b/src/vs/base/common/paths.ts @@ -27,7 +27,7 @@ function isPathSeparator(code: number) { * @param separator the separator to use * @returns the directory name of a path. * '.' is returned for empty paths or single segment relative paths (as done by NodeJS) - * For paths consisting only of a root, the inout path is returned + * For paths consisting only of a root, the input path is returned */ export function dirname(path: string, separator = nativeSep): string { const len = path.length; diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index 36b95e0f378..15de8c293f6 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -72,6 +72,26 @@ export function getWindowsShell(): string { return process.env['comspec'] || 'cmd.exe'; } +/** + * Sanitizes a VS Code process environment by removing all Electron/VS Code-related values. + */ +export function sanitizeProcessEnvironment(env: Platform.IProcessEnvironment): void { + const keysToRemove = [ + /^ELECTRON_.+$/, + /^GOOGLE_API_KEY$/, + /^VSCODE_.+$/ + ]; + const envKeys = Object.keys(env); + envKeys.forEach(envKey => { + for (let i = 0; i < keysToRemove.length; i++) { + if (envKey.search(keysToRemove[i]) !== -1) { + delete env[envKey]; + break; + } + } + }); +} + export abstract class AbstractProcess { private cmd: string; private args: string[]; diff --git a/src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts b/src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts index e90f23301a4..17ae7bb81b6 100644 --- a/src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts +++ b/src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import { IPCServer, ClientConnectionEvent } from 'vs/base/parts/ipc/node/ipc'; import { Protocol } from 'vs/base/parts/ipc/node/ipc.electron'; import { ipcMain } from 'electron'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; interface IIPCEvent { event: { sender: Electron.WebContents; }; @@ -21,12 +22,24 @@ function createScopedOnMessageEvent(senderId: number, eventName: string): Event< export class Server extends IPCServer { + private static Clients = new Map(); + private static getOnDidClientConnect(): Event { const onHello = Event.fromNodeEventEmitter(ipcMain, 'ipc:hello', ({ sender }) => sender); return Event.map(onHello, webContents => { - const onMessage = createScopedOnMessageEvent(webContents.id, 'ipc:message'); - const onDidClientDisconnect = Event.signal(createScopedOnMessageEvent(webContents.id, 'ipc:disconnect')); + const id = webContents.id; + const client = Server.Clients.get(id); + + if (client) { + client.dispose(); + } + + const onDidClientReconnect = new Emitter(); + Server.Clients.set(id, toDisposable(() => onDidClientReconnect.fire())); + + const onMessage = createScopedOnMessageEvent(id, 'ipc:message'); + const onDidClientDisconnect = Event.any(Event.signal(createScopedOnMessageEvent(id, 'ipc:disconnect')), onDidClientReconnect.event); const protocol = new Protocol(webContents, onMessage); return { protocol, onDidClientDisconnect }; diff --git a/src/vs/base/parts/tree/browser/tree.ts b/src/vs/base/parts/tree/browser/tree.ts index 1de72e5670b..a74f5cad56e 100644 --- a/src/vs/base/parts/tree/browser/tree.ts +++ b/src/vs/base/parts/tree/browser/tree.ts @@ -78,11 +78,6 @@ export interface ITree { */ refresh(element?: any, recursive?: boolean): Promise; - /** - * Updates an element's width. - */ - updateWidth(element: any): void; - /** * Expands an element. * The returned promise returns a boolean for whether the element was expanded or not. diff --git a/src/vs/base/parts/tree/browser/treeImpl.ts b/src/vs/base/parts/tree/browser/treeImpl.ts index 652c59d17a0..1b6c0f28da6 100644 --- a/src/vs/base/parts/tree/browser/treeImpl.ts +++ b/src/vs/base/parts/tree/browser/treeImpl.ts @@ -161,13 +161,6 @@ export class Tree implements _.ITree { return this.model.refresh(element, recursive); } - public updateWidth(element: any): void { - const item = this.model.getItem(element); - if (item) { - this.view.updateWidth(item); - } - } - public expand(element: any): Promise { return this.model.expand(element); } diff --git a/src/vs/base/parts/tree/browser/treeView.ts b/src/vs/base/parts/tree/browser/treeView.ts index 110ff2f8e53..fa0a7dcbe2b 100644 --- a/src/vs/base/parts/tree/browser/treeView.ts +++ b/src/vs/base/parts/tree/browser/treeView.ts @@ -1043,21 +1043,6 @@ export class TreeView extends HeightMap { } } - public updateWidth(item: Model.Item): void { - if (!item || !item.isVisible()) { - return; - } - - const viewItem = this.items[item.id]; - - if (!viewItem) { - return; - } - - viewItem.updateWidth(); - this.updateScrollWidth(); - } - public getRelativeTop(item: Model.Item): number { if (item && item.isVisible()) { let viewItem = this.items[item.id]; diff --git a/src/vs/base/test/common/filters.test.ts b/src/vs/base/test/common/filters.test.ts index d8f84b0aced..eec772c9263 100644 --- a/src/vs/base/test/common/filters.test.ts +++ b/src/vs/base/test/common/filters.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IFilter, or, matchesPrefix, matchesStrictPrefix, matchesCamelCase, matchesSubString, matchesContiguousSubString, matchesWords, fuzzyScore, IMatch, fuzzyScoreGraceful, fuzzyScoreGracefulAggressive, FuzzyScorer } from 'vs/base/common/filters'; +import { IFilter, or, matchesPrefix, matchesStrictPrefix, matchesCamelCase, matchesSubString, matchesContiguousSubString, matchesWords, fuzzyScore, IMatch, fuzzyScoreGraceful, fuzzyScoreGracefulAggressive, FuzzyScorer, createMatches } from 'vs/base/common/filters'; function filterOk(filter: IFilter, word: string, wordToMatchAgainst: string, highlights?: { start: number; end: number; }[]) { let r = filter(word, wordToMatchAgainst); @@ -208,14 +208,15 @@ suite('Filters', () => { let r = filter(pattern, pattern.toLowerCase(), opts.patternPos || 0, word, word.toLowerCase(), opts.wordPos || 0, opts.firstMatchCanBeWeak || false); assert.ok(!decoratedWord === !r); if (r) { - let [, matches] = r; + let matches = createMatches(r); let actualWord = ''; - for (let pos = 0; pos < word.length; pos++) { - if (2 ** pos & matches) { - actualWord += '^'; - } - actualWord += word[pos]; + let pos = 0; + for (const match of matches) { + actualWord += word.substring(pos, match.start); + actualWord += '^' + word.substring(match.start, match.end).split('').join('^'); + pos = match.end; } + actualWord += word.substring(pos); assert.equal(actualWord, decoratedWord); } } @@ -449,4 +450,8 @@ suite('Filters', () => { assertMatches('cno', 'co_new', '^c^o_^new', fuzzyScoreGraceful); assertMatches('cno', 'co_new', '^c^o_^new', fuzzyScoreGracefulAggressive); }); + + test('List highlight filter: Not all characters from match are highlighterd #66923', () => { + assertMatches('foo', 'barbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar_foo', 'barbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar_^f^o^o', fuzzyScore); + }); }); diff --git a/src/vs/base/test/node/processes/processes.test.ts b/src/vs/base/test/node/processes/processes.test.ts index 76719506d6e..10199cf5bf3 100644 --- a/src/vs/base/test/node/processes/processes.test.ts +++ b/src/vs/base/test/node/processes/processes.test.ts @@ -84,4 +84,29 @@ suite('Processes', () => { } }); }); + + + test('sanitizeProcessEnvironment', () => { + let env = { + FOO: 'bar', + ELECTRON_ENABLE_STACK_DUMPING: 'x', + ELECTRON_ENABLE_LOGGING: 'x', + 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', + VSCODE_LOGS: 'x', + VSCODE_NLS_CONFIG: 'x', + VSCODE_PORTABLE: 'x', + VSCODE_PID: 'x', + VSCODE_NODE_CACHED_DATA_DIR: 'x', + VSCODE_NEW_VAR: 'x' + }; + processes.sanitizeProcessEnvironment(env); + assert.equal(env['FOO'], 'bar'); + assert.equal(Object.keys(env).length, 1); + }); }); diff --git a/src/vs/code/electron-browser/processExplorer/media/processExplorer.css b/src/vs/code/electron-browser/processExplorer/media/processExplorer.css index 312929a2b46..2691208f2a2 100644 --- a/src/vs/code/electron-browser/processExplorer/media/processExplorer.css +++ b/src/vs/code/electron-browser/processExplorer/media/processExplorer.css @@ -56,7 +56,7 @@ table { width: 100%; table-layout: fixed; } -th { +th[scope='col'] { vertical-align: bottom; border-bottom: 1px solid #cccccc; padding: .5rem; @@ -80,6 +80,9 @@ td { .data { white-space: pre; padding-left: .5rem; + font-weight: normal; + text-align: left; + height: 22px; } tbody > tr:hover { diff --git a/src/vs/code/electron-browser/processExplorer/processExplorer.html b/src/vs/code/electron-browser/processExplorer/processExplorer.html index 366bdda9d4e..3ef3be23f8f 100644 --- a/src/vs/code/electron-browser/processExplorer/processExplorer.html +++ b/src/vs/code/electron-browser/processExplorer/processExplorer.html @@ -8,7 +8,7 @@ -
+
diff --git a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts index a3a47fd39a9..a9e8f38c810 100644 --- a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts @@ -113,42 +113,56 @@ function getProcessIdWithHighestProperty(processList, propertyName: string) { } function updateProcessInfo(processList): void { - const target = document.getElementById('process-list'); - if (!target) { + const container = document.getElementById('process-list'); + if (!container) { return; } + container.innerHTML = ''; const highestCPUProcess = getProcessIdWithHighestProperty(processList, 'cpu'); const highestMemoryProcess = getProcessIdWithHighestProperty(processList, 'memory'); - let tableHtml = ` - - - ${localize('cpu', "CPU %")} - ${localize('memory', "Memory (MB)")} - ${localize('pid', "pid")} - ${localize('name', "Name")} - - `; + const tableHead = document.createElement('thead'); + tableHead.innerHTML = ` + ${localize('cpu', "CPU %")} + ${localize('memory', "Memory (MB)")} + ${localize('pid', "pid")} + ${localize('name', "Name")} + `; - tableHtml += ``; + const tableBody = document.createElement('tbody'); processList.forEach(p => { - const cpuClass = p.pid === highestCPUProcess ? 'highest' : ''; - const memoryClass = p.pid === highestMemoryProcess ? 'highest' : ''; + const row = document.createElement('tr'); + row.id = p.pid; - tableHtml += ` - - ${p.cpu} - ${p.memory} - ${p.pid} - ${p.formattedName} - `; + const cpu = document.createElement('td'); + p.pid === highestCPUProcess + ? cpu.classList.add('centered', 'highest') + : cpu.classList.add('centered'); + cpu.textContent = p.cpu; + + const memory = document.createElement('td'); + p.pid === highestMemoryProcess + ? memory.classList.add('centered', 'highest') + : memory.classList.add('centered'); + memory.textContent = p.memory; + + 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); + tableBody.appendChild(row); }); - tableHtml += ``; - - target.innerHTML = tableHtml; + container.append(tableHead, tableBody); } function applyStyles(styles: ProcessExplorerStyles): void { diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 7c52379bc9b..a342222d046 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -123,7 +123,7 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I } server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(appInsightsAppender)); - services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService, [false])); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index d72458428f7..dbe19cc5743 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -424,34 +424,6 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Handle Workspace events this._register(this.workspacesMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e))); - - // TODO@Ben workaround for https://github.com/Microsoft/vscode/issues/13612 - // It looks like smooth scrolling disappears as soon as the window is minimized - // and maximized again. Touching some window properties "fixes" it, like toggling - // the visibility of the menu. - if (isWindows) { - const windowConfig = this.configurationService.getValue('window'); - if (windowConfig && windowConfig.smoothScrollingWorkaround === true) { - let minimized = false; - - const restoreSmoothScrolling = () => { - if (minimized) { - const visibility = this.getMenuBarVisibility(); - const temporaryVisibility: MenuBarVisibility = (visibility === 'hidden' || visibility === 'toggle') ? 'default' : 'hidden'; - setTimeout(() => { - this.doSetMenuBarVisibility(temporaryVisibility); - this.doSetMenuBarVisibility(visibility); - }, 0); - } - - minimized = false; - }; - - this._win.on('minimize', () => minimized = true); - this._win.on('restore', () => restoreSmoothScrolling()); - this._win.on('maximize', () => restoreSmoothScrolling()); - } - } } private onUntitledWorkspaceDeleted(workspace: IWorkspaceIdentifier): void { @@ -559,6 +531,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } reload(configurationIn?: IWindowConfiguration, cli?: ParsedArgs): void { + // If config is not provided, copy our current one const configuration = configurationIn ? configurationIn : objects.mixin({}, this.currentConfig); diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 563bb5f2d8f..9c2ff750b69 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -13,7 +13,7 @@ import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/ import { IStateService } from 'vs/platform/state/common/state'; import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window'; import { hasArgs, asArray } from 'vs/platform/environment/node/argv'; -import { ipcMain as ipc, screen, BrowserWindow, dialog, systemPreferences, app } from 'electron'; +import { ipcMain as ipc, screen, BrowserWindow, dialog, systemPreferences } from 'electron'; import { IPathWithLineAndColumn, parseLineAndColumnAware } from 'vs/code/node/paths'; import { ILifecycleService, UnloadReason, IWindowUnloadEvent, LifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -26,7 +26,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; import { IHistoryMainService } from 'vs/platform/history/common/history'; import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_FILTER, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_FILTER, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { Schemas } from 'vs/base/common/network'; @@ -34,7 +34,7 @@ import { normalizeNFC } from 'vs/base/common/normalization'; import { URI } from 'vs/base/common/uri'; import { Queue, timeout } from 'vs/base/common/async'; import { exists } from 'vs/base/node/pfs'; -import { getComparisonKey, isEqual, normalizePath } from 'vs/base/common/resources'; +import { getComparisonKey, isEqual, normalizePath, basename as resourcesBasename } from 'vs/base/common/resources'; import { endsWith } from 'vs/base/common/strings'; import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; @@ -145,12 +145,6 @@ export class WindowsManager implements IWindowsMainService { private _onWindowLoad = new Emitter(); onWindowLoad: CommonEvent = this._onWindowLoad.event; - private _onActiveWindowChanged = new Emitter(); - onActiveWindowChanged: CommonEvent = this._onActiveWindowChanged.event; - - private _onWindowReload = new Emitter(); - onWindowReload: CommonEvent = this._onWindowReload.event; - private _onWindowsCountChanged = new Emitter(); onWindowsCountChanged: CommonEvent = this._onWindowsCountChanged.event; @@ -173,7 +167,7 @@ export class WindowsManager implements IWindowsMainService { } this.dialogs = new Dialogs(environmentService, telemetryService, stateService, this); - this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, environmentService, this); + this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, environmentService, historyMainService, this); } private getWindowsState(): IWindowsState { @@ -208,13 +202,6 @@ export class WindowsManager implements IWindowsMainService { private registerListeners(): void { - // React to windows focus changes - app.on('browser-window-focus', () => { - setTimeout(() => { - this._onActiveWindowChanged.fire(this.getLastActiveWindow()); - }); - }); - // React to workbench ready events from windows ipc.on('vscode:workbenchReady', (event: any, windowId: number) => { this.logService.trace('IPC#vscode-workbenchReady'); @@ -831,7 +818,7 @@ export class WindowsManager implements IWindowsMainService { if (!openConfig.addMode && isCommandLineOrAPICall) { const foldersToOpen = windowsToOpen.filter(path => !!path.folderUri); if (foldersToOpen.length > 1 && foldersToOpen.every(f => f.folderUri.scheme === Schemas.file)) { - const workspace = this.workspacesMainService.createWorkspaceSync(foldersToOpen.map(folder => ({ uri: folder.folderUri }))); + const workspace = this.workspacesMainService.createUntitledWorkspaceSync(foldersToOpen.map(folder => ({ uri: folder.folderUri }))); // Add workspace and remove folders thereby windowsToOpen.push({ workspace, remoteAuthority: foldersToOpen[0].remoteAuthority }); @@ -1471,9 +1458,6 @@ export class WindowsManager implements IWindowsMainService { this.lifecycleService.unload(win, UnloadReason.RELOAD).then(veto => { if (!veto) { win.reload(undefined, cli); - - // Emit - this._onWindowReload.fire(win.id); } }); } @@ -1486,18 +1470,10 @@ export class WindowsManager implements IWindowsMainService { }); } - saveAndEnterWorkspace(win: ICodeWindow, path: string): Promise { - return this.workspacesManager.saveAndEnterWorkspace(win, path).then(result => this.doEnterWorkspace(win, result)); - } - - enterWorkspace(win: ICodeWindow, path: string): Promise { + enterWorkspace(win: ICodeWindow, path: URI): Promise { return this.workspacesManager.enterWorkspace(win, path).then(result => this.doEnterWorkspace(win, result)); } - createAndEnterWorkspace(win: ICodeWindow, folders?: IWorkspaceFolderCreationData[], path?: string): Promise { - return this.workspacesManager.createAndEnterWorkspace(win, folders, path).then(result => this.doEnterWorkspace(win, result)); - } - private doEnterWorkspace(win: ICodeWindow, result: IEnterWorkspaceResult): IEnterWorkspaceResult { // Mark as recently opened @@ -1979,19 +1955,12 @@ class WorkspacesManager { private workspacesMainService: IWorkspacesMainService, private backupMainService: IBackupMainService, private environmentService: IEnvironmentService, - private windowsMainService: IWindowsMainService + private historyMainService: IHistoryMainService, + private windowsMainService: IWindowsMainService, ) { } - saveAndEnterWorkspace(window: ICodeWindow, path: string): Promise { - if (!window || !window.win || !window.isReady || !window.openedWorkspace || !path || !this.isValidTargetWorkspacePath(window, path)) { - return Promise.resolve(null); // return early if the window is not ready or disposed or does not have a workspace - } - - return this.doSaveAndOpenWorkspace(window, window.openedWorkspace, path); - } - - enterWorkspace(window: ICodeWindow, path: string): Promise { + enterWorkspace(window: ICodeWindow, path: URI): Promise { if (!window || !window.win || !window.isReady) { return Promise.resolve(null); // return early if the window is not ready or disposed } @@ -2000,47 +1969,28 @@ class WorkspacesManager { if (!isValid) { return null; // return early if the workspace is not valid } - - return this.workspacesMainService.resolveWorkspace(path).then(workspace => { - return this.doOpenWorkspace(window, workspace); - }); + const workspaceIdentifier = this.workspacesMainService.getWorkspaceIdentifier(path); + return this.doOpenWorkspace(window, workspaceIdentifier); }); } - createAndEnterWorkspace(window: ICodeWindow, folders?: IWorkspaceFolderCreationData[], path?: string): Promise { - if (!window || !window.win || !window.isReady) { - return Promise.resolve(null); // return early if the window is not ready or disposed - } - - return this.isValidTargetWorkspacePath(window, path).then(isValid => { - if (!isValid) { - return null; // return early if the workspace is not valid - } - - return this.workspacesMainService.createWorkspace(folders).then(workspace => { - return this.doSaveAndOpenWorkspace(window, workspace, path); - }); - }); - - } - - private isValidTargetWorkspacePath(window: ICodeWindow, path?: string): Promise { + private isValidTargetWorkspacePath(window: ICodeWindow, path?: URI): Promise { if (!path) { return Promise.resolve(true); } - if (window.openedWorkspace && window.openedWorkspace.configPath === path) { + if (window.openedWorkspace && isEqual(URI.file(window.openedWorkspace.configPath), path)) { return Promise.resolve(false); // window is already opened on a workspace with that path } // Prevent overwriting a workspace that is currently opened in another window - if (findWindowOnWorkspace(this.windowsMainService.getWindows(), { id: this.workspacesMainService.getWorkspaceId(path), configPath: path })) { + if (findWindowOnWorkspace(this.windowsMainService.getWindows(), this.workspacesMainService.getWorkspaceIdentifier(path))) { const options: Electron.MessageBoxOptions = { title: product.nameLong, type: 'info', buttons: [localize('ok', "OK")], - message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)), + message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", resourcesBasename(path)), detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), noLink: true }; @@ -2051,17 +2001,6 @@ class WorkspacesManager { return Promise.resolve(true); // OK } - private doSaveAndOpenWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier, path?: string): Promise { - let savePromise: Promise; - if (path) { - savePromise = this.workspacesMainService.saveWorkspace(workspace, path); - } else { - savePromise = Promise.resolve(workspace); - } - - return savePromise.then(workspace => this.doOpenWorkspace(window, workspace)); - } - private doOpenWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult { window.focus(); @@ -2071,6 +2010,11 @@ class WorkspacesManager { backupPath = this.backupMainService.registerWorkspaceBackupSync(workspace, window.config.backupPath); } + // if the window was opened on an untitled workspace, delete it. + if (window.openedWorkspace && this.workspacesMainService.isUntitledWorkspace(window.openedWorkspace)) { + this.workspacesMainService.deleteUntitledWorkspaceSync(window.openedWorkspace); + } + // Update window configuration properly based on transition to workspace window.config.folderUri = undefined; window.config.workspace = workspace; @@ -2152,7 +2096,11 @@ class WorkspacesManager { defaultPath: this.getUntitledWorkspaceSaveDialogDefaultPath(workspace) }, window).then(target => { if (target) { - return this.workspacesMainService.saveWorkspace(workspace, target).then(() => false, () => false); + return this.workspacesMainService.saveWorkspaceAs(workspace, target).then(savedWorkspace => { + this.historyMainService.addRecentlyOpened([savedWorkspace], []); + this.workspacesMainService.deleteUntitledWorkspaceSync(workspace); + return false; + }, () => false); } return true; // keep veto if no target was provided diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 50b202a6a64..98b1c6dfe3f 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -136,7 +136,7 @@ export class Main { const [id, version] = getIdAndVersion(extension); return this.extensionManagementService.getInstalled(ExtensionType.User) - .then(installed => this.extensionGalleryService.getExtension({ id }, version) + .then(installed => this.extensionGalleryService.getCompatibleExtension({ id }, version) .then(null, err => { if (err.responseText) { try { @@ -268,7 +268,7 @@ export function main(argv: ParsedArgs): Promise { const services = new ServiceCollection(); services.set(IConfigurationService, new SyncDescriptor(ConfigurationService)); services.set(IRequestService, new SyncDescriptor(RequestService)); - services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService, [false])); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); const appenders: AppInsightsAppender[] = []; diff --git a/src/vs/editor/browser/editorExtensions.ts b/src/vs/editor/browser/editorExtensions.ts index 071893e858b..15365b311a9 100644 --- a/src/vs/editor/browser/editorExtensions.ts +++ b/src/vs/editor/browser/editorExtensions.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { IPosition } from 'vs/base/browser/ui/contextview/contextview'; -import { always } from 'vs/base/common/async'; import { illegalArgument } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -266,14 +265,14 @@ export function registerDefaultLanguageCommand(id: string, handler: (model: ITex } return accessor.get(ITextModelService).createModelReference(resource).then(reference => { - return always(new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { try { let result = handler(reference.object.textEditorModel, Position.lift(position), args); resolve(result); } catch (err) { reject(err); } - }), () => { + }).finally(() => { reference.dispose(); }); }); diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index ae8a3b77861..1468afc9581 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1229,6 +1229,14 @@ export interface NewCommentAction { actions: Command[]; } +/** + * @internal + */ +export interface CommentReaction { + readonly label?: string; + readonly hasReacted?: boolean; +} + /** * @internal */ @@ -1241,6 +1249,7 @@ export interface Comment { readonly canDelete?: boolean; readonly command?: Command; readonly isDraft?: boolean; + readonly commentReactions?: CommentReaction[]; } /** @@ -1284,6 +1293,11 @@ export interface DocumentCommentProvider { startDraftLabel?: string; deleteDraftLabel?: string; finishDraftLabel?: string; + + addReaction?(resource: URI, comment: Comment, reaction: CommentReaction, token: CancellationToken): Promise; + deleteReaction?(resource: URI, comment: Comment, reaction: CommentReaction, token: CancellationToken): Promise; + reactionGroup?: CommentReaction[]; + onDidChangeCommentThreads(): Event; } diff --git a/src/vs/editor/contrib/codeAction/codeAction.ts b/src/vs/editor/contrib/codeAction/codeAction.ts index 5282fbb9dc7..e6015b5d02b 100644 --- a/src/vs/editor/contrib/codeAction/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/codeAction.ts @@ -13,7 +13,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ITextModel } from 'vs/editor/common/model'; import { CodeAction, CodeActionContext, CodeActionProviderRegistry, CodeActionTrigger as CodeActionTriggerKind } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { CodeActionKind, CodeActionTrigger, filtersAction, mayIncludeActionsOfKind } from './codeActionTrigger'; +import { CodeActionKind, CodeActionTrigger, filtersAction, mayIncludeActionsOfKind, CodeActionFilter } from './codeActionTrigger'; export function getCodeActions( model: ITextModel, @@ -32,35 +32,40 @@ export function getCodeActions( rangeOrSelection = model.getFullModelRange(); } - const promises = CodeActionProviderRegistry.all(model) - // Avoid calling providers that we know will not return code actions of interest + const promises = getCodeActionProviders(model, filter).map(provider => { + return Promise.resolve(provider.provideCodeActions(model, rangeOrSelection, codeActionContext, token)).then(providedCodeActions => { + if (!Array.isArray(providedCodeActions)) { + return []; + } + return providedCodeActions.filter(action => action && filtersAction(filter, action)); + }, (err): CodeAction[] => { + if (isPromiseCanceledError(err)) { + throw err; + } + + onUnexpectedExternalError(err); + return []; + }); + }); + + return Promise.all(promises) + .then(flatten) + .then(allCodeActions => mergeSort(allCodeActions, codeActionsComparator)); +} + +function getCodeActionProviders( + model: ITextModel, + filter: CodeActionFilter +) { + return CodeActionProviderRegistry.all(model) + // Don't include providers that we know will not return code actions of interest .filter(provider => { if (!provider.providedCodeActionKinds) { // We don't know what type of actions this provider will return. return true; } - return provider.providedCodeActionKinds.some(kind => mayIncludeActionsOfKind(filter, new CodeActionKind(kind))); - }) - .map(support => { - return Promise.resolve(support.provideCodeActions(model, rangeOrSelection, codeActionContext, token)).then(providedCodeActions => { - if (!Array.isArray(providedCodeActions)) { - return []; - } - return providedCodeActions.filter(action => action && filtersAction(filter, action)); - }, (err): CodeAction[] => { - if (isPromiseCanceledError(err)) { - throw err; - } - - onUnexpectedExternalError(err); - return []; - }); }); - - return Promise.all(promises) - .then(flatten) - .then(allCodeActions => mergeSort(allCodeActions, codeActionsComparator)); } function codeActionsComparator(a: CodeAction, b: CodeAction): number { diff --git a/src/vs/editor/contrib/codeAction/codeActionWidget.ts b/src/vs/editor/contrib/codeAction/codeActionWidget.ts index 7b4b5fb5acb..34bf98be6b7 100644 --- a/src/vs/editor/contrib/codeAction/codeActionWidget.ts +++ b/src/vs/editor/contrib/codeAction/codeActionWidget.ts @@ -5,7 +5,6 @@ import { getDomNodePagePosition } from 'vs/base/browser/dom'; import { Action } from 'vs/base/common/actions'; -import { always } from 'vs/base/common/async'; import { canceled } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -53,11 +52,9 @@ export class CodeActionContextMenu { private codeActionToAction(action: CodeAction): Action { const id = action.command ? action.command.id : action.title; const title = action.isPreferred ? `${action.title} ★` : action.title; - return new Action(id, title, undefined, true, () => { - return always( - this._onApplyCodeAction(action), - () => this._onDidExecuteCodeAction.fire(undefined)); - }); + return new Action(id, title, undefined, true, () => + this._onApplyCodeAction(action) + .finally(() => this._onDidExecuteCodeAction.fire(undefined))); } get isVisible(): boolean { diff --git a/src/vs/editor/contrib/documentSymbols/outlineModel.ts b/src/vs/editor/contrib/documentSymbols/outlineModel.ts index 4373e21fbec..cf559331c52 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineModel.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineModel.ts @@ -20,12 +20,14 @@ export abstract class TreeElement { abstract id: string; abstract children: { [id: string]: TreeElement }; - abstract parent: TreeElement; + abstract parent: TreeElement | undefined; abstract adopt(newParent: TreeElement): TreeElement; remove(): void { - delete this.parent.children[this.id]; + if (this.parent) { + delete this.parent.children[this.id]; + } } static findId(candidate: DocumentSymbol | string, container: TreeElement): string { @@ -88,18 +90,18 @@ export abstract class TreeElement { export class OutlineElement extends TreeElement { children: { [id: string]: OutlineElement; } = Object.create(null); - score: FuzzyScore = FuzzyScore.Default; - marker: { count: number, topSev: MarkerSeverity }; + score: FuzzyScore | undefined = FuzzyScore.Default; + marker: { count: number, topSev: MarkerSeverity } | undefined; constructor( readonly id: string, - public parent: OutlineModel | OutlineGroup | OutlineElement, + public parent: TreeElement | undefined, readonly symbol: DocumentSymbol ) { super(); } - adopt(parent: OutlineModel | OutlineGroup | OutlineElement): OutlineElement { + adopt(parent: TreeElement): OutlineElement { let res = new OutlineElement(this.id, parent, this.symbol); forEach(this.children, entry => res.children[entry.key] = entry.value.adopt(res)); return res; @@ -112,33 +114,33 @@ export class OutlineGroup extends TreeElement { constructor( readonly id: string, - public parent: OutlineModel, + public parent: TreeElement | undefined, readonly provider: DocumentSymbolProvider, readonly providerIndex: number, ) { super(); } - adopt(parent: OutlineModel): OutlineGroup { + adopt(parent: TreeElement): OutlineGroup { let res = new OutlineGroup(this.id, parent, this.provider, this.providerIndex); forEach(this.children, entry => res.children[entry.key] = entry.value.adopt(res)); return res; } - updateMatches(pattern: string, topMatch: OutlineElement): OutlineElement { + updateMatches(pattern: string, topMatch: OutlineElement | undefined): OutlineElement | undefined { for (const key in this.children) { topMatch = this._updateMatches(pattern, this.children[key], topMatch); } return topMatch; } - private _updateMatches(pattern: string, item: OutlineElement, topMatch: OutlineElement): OutlineElement { + private _updateMatches(pattern: string, item: OutlineElement, topMatch: OutlineElement | undefined): OutlineElement | undefined { item.score = pattern ? fuzzyScore(pattern, pattern.toLowerCase(), 0, item.symbol.name, item.symbol.name.toLowerCase(), 0, true) : FuzzyScore.Default; - if (item.score && (!topMatch || item.score[0] > topMatch.score[0])) { + if (item.score && (!topMatch || !topMatch.score || item.score[0] > topMatch.score[0])) { topMatch = item; } for (const key in item.children) { @@ -152,11 +154,11 @@ export class OutlineGroup extends TreeElement { return topMatch; } - getItemEnclosingPosition(position: IPosition): OutlineElement { + getItemEnclosingPosition(position: IPosition): OutlineElement | undefined { return position ? this._getItemEnclosingPosition(position, this.children) : undefined; } - private _getItemEnclosingPosition(position: IPosition, children: { [id: string]: OutlineElement }): OutlineElement { + private _getItemEnclosingPosition(position: IPosition, children: { [id: string]: OutlineElement }): OutlineElement | undefined { for (let key in children) { let item = children[key]; if (!item.symbol.range || !Range.containsPosition(item.symbol.range, position)) { @@ -174,7 +176,6 @@ export class OutlineGroup extends TreeElement { } private _updateMarker(markers: IMarker[], item: OutlineElement): void { - item.marker = undefined; // find the proper start index to check for item/marker overlap. @@ -190,14 +191,14 @@ export class OutlineGroup extends TreeElement { } let myMarkers: IMarker[] = []; - let myTopSev: MarkerSeverity; + let myTopSev: MarkerSeverity | undefined; for (; start < markers.length && Range.areIntersecting(item.symbol.range, markers[start]); start++) { // remove markers intersecting with this outline element // and store them in a 'private' array. let marker = markers[start]; myMarkers.push(marker); - markers[start] = undefined; + (markers as Array)[start] = undefined; if (!myTopSev || marker.severity > myTopSev) { myTopSev = marker.severity; } @@ -224,7 +225,7 @@ export class OutlineGroup extends TreeElement { export class OutlineModel extends TreeElement { - private static readonly _requests = new LRUCache, model: OutlineModel }>(9, 0.75); + private static readonly _requests = new LRUCache, model: OutlineModel | undefined }>(9, 0.75); private static readonly _keys = new class { private _counter = 1; @@ -265,25 +266,25 @@ export class OutlineModel extends TreeElement { OutlineModel._requests.set(key, data); } - if (data.model) { + if (data!.model) { // resolved -> return data return Promise.resolve(data.model); } // increase usage counter - data.promiseCnt += 1; + data!.promiseCnt += 1; token.onCancellationRequested(() => { // last -> cancel provider request, remove cached promise - if (--data.promiseCnt === 0) { - data.source.cancel(); + if (--data!.promiseCnt === 0) { + data!.source.cancel(); OutlineModel._requests.delete(key); } }); return new Promise((resolve, reject) => { - data.promise.then(model => { - data.model = model; + data!.promise.then(model => { + data!.model = model; resolve(model); }, err => { OutlineModel._requests.delete(key); @@ -331,7 +332,7 @@ export class OutlineModel extends TreeElement { container.children[res.id] = res; } - static get(element: TreeElement): OutlineModel { + static get(element: TreeElement | undefined): OutlineModel | undefined { while (element) { if (element instanceof OutlineModel) { return element; @@ -373,8 +374,8 @@ export class OutlineModel extends TreeElement { } else { // adopt all elements of the first group let group = first(this._groups); - for (let key in group.children) { - let child = group.children[key]; + for (let key in group!.children) { + let child = group!.children[key]; child.parent = this; this.children[child.id] = child; } @@ -394,13 +395,13 @@ export class OutlineModel extends TreeElement { return true; } - private _matches: [string, OutlineElement]; + private _matches: [string, OutlineElement | undefined]; - updateMatches(pattern: string): OutlineElement { + updateMatches(pattern: string): OutlineElement | undefined { if (this._matches && this._matches[0] === pattern) { return this._matches[1]; } - let topMatch: OutlineElement; + let topMatch: OutlineElement | undefined; for (const key in this._groups) { topMatch = this._groups[key].updateMatches(pattern, topMatch); } @@ -408,9 +409,9 @@ export class OutlineModel extends TreeElement { return topMatch; } - getItemEnclosingPosition(position: IPosition, context?: OutlineElement): OutlineElement { + getItemEnclosingPosition(position: IPosition, context?: OutlineElement): OutlineElement | undefined { - let preferredGroup: OutlineGroup; + let preferredGroup: OutlineGroup | undefined; if (context) { let candidate = context.parent; while (candidate && !preferredGroup) { @@ -432,7 +433,7 @@ export class OutlineModel extends TreeElement { return result; } - getItemById(id: string): TreeElement { + getItemById(id: string): TreeElement | undefined { return TreeElement.getElementById(id, this); } diff --git a/src/vs/editor/contrib/format/formatActions.ts b/src/vs/editor/contrib/format/formatActions.ts index 6cbb2a2aeb9..429ea13fa61 100644 --- a/src/vs/editor/contrib/format/formatActions.ts +++ b/src/vs/editor/contrib/format/formatActions.ts @@ -74,8 +74,8 @@ export function formatDocumentRange(telemetryService: ITelemetryService, workerS if (provider.length !== 1) { /* __GDPR__ "manyformatters" : { - "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - "language" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "language" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } } */ @@ -143,8 +143,8 @@ export function formatDocument(telemetryService: ITelemetryService, workerServic if (provider.length !== 1) { /* __GDPR__ "manyformatters" : { - "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - "language" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "language" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } } */ diff --git a/src/vs/editor/contrib/referenceSearch/referencesTree.ts b/src/vs/editor/contrib/referenceSearch/referencesTree.ts index e9282d70092..296d55db965 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesTree.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesTree.ts @@ -20,7 +20,7 @@ import { escape } from 'vs/base/common/strings'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { IListVirtualDelegate, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list'; +import { IListVirtualDelegate, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { basename } from 'vs/base/common/paths'; @@ -93,6 +93,13 @@ export class StringRepresentationProvider implements IKeyboardNavigationLabelPro } } +export class IdentityProvider implements IIdentityProvider { + + getId(element: TreeElement): { toString(): string; } { + return element.id; + } +} + //#region render: File class FileReferencesTemplate extends Disposable { diff --git a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts index a5e63476dbb..1978ad1e501 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts @@ -21,7 +21,7 @@ import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/ import { ModelDecorationOptions, TextModel } from 'vs/editor/common/model/textModel'; import { Location } from 'vs/editor/common/modes'; import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; -import { AriaProvider, DataSource, Delegate, FileReferencesRenderer, OneReferenceRenderer, TreeElement, StringRepresentationProvider } from 'vs/editor/contrib/referenceSearch/referencesTree'; +import { AriaProvider, DataSource, Delegate, FileReferencesRenderer, OneReferenceRenderer, TreeElement, StringRepresentationProvider, IdentityProvider } from 'vs/editor/contrib/referenceSearch/referencesTree'; import * as nls from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -358,11 +358,12 @@ export class ReferenceWidget extends PeekViewWidget { this._instantiationService.createInstance(OneReferenceRenderer), ]; - const treeOptions = { + const treeOptions: IAsyncDataTreeOptions = { ariaLabel: nls.localize('treeAriaLabel', "References"), keyboardSupport: this._defaultTreeKeyboardSupport, accessibilityProvider: new AriaProvider(), - keyboardNavigationLabelProvider: this._instantiationService.createInstance(StringRepresentationProvider) + keyboardNavigationLabelProvider: this._instantiationService.createInstance(StringRepresentationProvider), + identityProvider: new IdentityProvider() }; const treeDataSource = this._instantiationService.createInstance(DataSource); @@ -427,7 +428,7 @@ export class ReferenceWidget extends PeekViewWidget { this._treeContainer.style.height = height; this._treeContainer.style.width = right; // forward - this._tree.layout(heightInPixel); + this._tree.layout(heightInPixel, widthInPixel); this._preview.layout(); // store layout data diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 347de4b6499..fc3f3d8f62a 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -157,7 +157,7 @@ class Renderer implements IListRenderer }; let color: string | null = null; - if (suggestion.kind === CompletionItemKind.Color && ((color = matchesColor(suggestion.label)) || typeof suggestion.documentation === 'string' && matchesColor(suggestion.documentation))) { + if (suggestion.kind === CompletionItemKind.Color && ((color = matchesColor(suggestion.label) || typeof suggestion.documentation === 'string' ? matchesColor(suggestion.documentation as any) : null))) { // special logic for 'color' completion items data.icon.className = 'icon customcolor'; data.colorspan.style.backgroundColor = color; diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index b463eae52f8..e92924064ef 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -521,6 +521,10 @@ export class SimpleWorkspaceContextService implements IWorkspaceContextService { this.workspace = { id: '4064f6ec-cb38-4ad0-af64-ee6467e63c82', folders: [new WorkspaceFolder({ uri: resource, name: '', index: 0 })] }; } + getCompleteWorkspace(): Promise { + return Promise.resolve(this.getWorkspace()); + } + public getWorkspace(): IWorkspace { return this.workspace; } diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 715c24c363b..71cf8c51599 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -122,13 +122,13 @@ export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { } export interface IStandaloneCodeEditor extends ICodeEditor { - addCommand(keybinding: number, handler: ICommandHandler, context: string): string | null; + addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null; createContextKey(key: string, defaultValue: T): IContextKey; addAction(descriptor: IActionDescriptor): IDisposable; } export interface IStandaloneDiffEditor extends IDiffEditor { - addCommand(keybinding: number, handler: ICommandHandler, context: string): string | null; + addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null; createContextKey(key: string, defaultValue: T): IContextKey; addAction(descriptor: IActionDescriptor): IDisposable; @@ -182,7 +182,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon createAriaDomNode(); } - public addCommand(keybinding: number, handler: ICommandHandler, context: string): string | null { + public addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null { if (!this._standaloneKeybindingService) { console.warn('Cannot add command because the editor is configured with an unrecognized KeybindingService'); return null; @@ -409,7 +409,7 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon return super.getModifiedEditor(); } - public addCommand(keybinding: number, handler: ICommandHandler, context: string): string | null { + public addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null { return this.getModifiedEditor().addCommand(keybinding, handler, context); } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 937ec711364..35793ae2e20 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1071,13 +1071,13 @@ declare namespace monaco.editor { } export interface IStandaloneCodeEditor extends ICodeEditor { - addCommand(keybinding: number, handler: ICommandHandler, context: string): string | null; + addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null; createContextKey(key: string, defaultValue: T): IContextKey; addAction(descriptor: IActionDescriptor): IDisposable; } export interface IStandaloneDiffEditor extends IDiffEditor { - addCommand(keybinding: number, handler: ICommandHandler, context: string): string | null; + addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null; createContextKey(key: string, defaultValue: T): IContextKey; addAction(descriptor: IActionDescriptor): IDisposable; getOriginalEditor(): IStandaloneCodeEditor; diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 5607fad1477..49b9378bb91 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -156,11 +156,11 @@ export interface IExtensionGalleryService { getManifest(extension: IGalleryExtension, token: CancellationToken): Promise; getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise; getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise; - loadCompatibleVersion(extension: IGalleryExtension, fromVersion?: string): Promise; getAllVersions(extension: IGalleryExtension, compatible: boolean): Promise; loadAllDependencies(dependencies: IExtensionIdentifier[], token: CancellationToken): Promise; getExtensionsReport(): Promise; - getExtension(id: IExtensionIdentifier, version?: string): Promise; + getCompatibleExtension(extension: IGalleryExtension): Promise; + getCompatibleExtension(id: IExtensionIdentifier, version?: string): Promise; } export interface InstallExtensionEvent { diff --git a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts index ef888757750..a72c577218f 100644 --- a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts @@ -7,7 +7,7 @@ import { tmpdir } from 'os'; import * as path from 'path'; import { distinct } from 'vs/base/common/arrays'; import { getErrorMessage, isPromiseCanceledError, canceled } from 'vs/base/common/errors'; -import { StatisticType, IGalleryExtension, IExtensionGalleryService, IGalleryExtensionAsset, IQueryOptions, SortBy, SortOrder, IExtensionIdentifier, IReportedExtension, InstallOperation, ITranslation, IGalleryExtensionVersion, IGalleryExtensionAssets } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { StatisticType, IGalleryExtension, IExtensionGalleryService, IGalleryExtensionAsset, IQueryOptions, SortBy, SortOrder, IExtensionIdentifier, IReportedExtension, InstallOperation, ITranslation, IGalleryExtensionVersion, IGalleryExtensionAssets, isIExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId, getGalleryExtensionTelemetryData, adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { assign, getOrDefault } from 'vs/base/common/objects'; import { IRequestService } from 'vs/platform/request/node/request'; @@ -351,12 +351,18 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return !!this.extensionsGalleryUrl; } - getExtension({ id, uuid }: IExtensionIdentifier, version?: string): Promise { + getCompatibleExtension(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise { + const extension: IGalleryExtension | null = isIExtensionIdentifier(arg1) ? null : arg1; + if (extension && extension.properties.engine && isEngineValid(extension.properties.engine)) { + return Promise.resolve(extension); + } + const { id, uuid } = extension ? extension.identifier : arg1; let query = new Query() .withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties, Flags.ExcludeNonValidated) .withPage(1, 1) .withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code') - .withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished)); + .withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished)) + .withAssetTypes(AssetType.Manifest, AssetType.VSIX); if (uuid) { query = query.withFilter(FilterType.ExtensionId, uuid); @@ -364,16 +370,30 @@ export class ExtensionGalleryService implements IExtensionGalleryService { query = query.withFilter(FilterType.ExtensionName, id); } - return this.queryGallery(query, CancellationToken.None).then(({ galleryExtensions }) => { - if (galleryExtensions.length) { - const galleryExtension = galleryExtensions[0]; - const versionAsset = version ? galleryExtension.versions.filter(v => v.version === version)[0] : galleryExtension.versions[0]; - if (versionAsset) { - return toExtension(galleryExtension, versionAsset, 0, query); + return this.queryGallery(query, CancellationToken.None) + .then(({ galleryExtensions }) => { + const [rawExtension] = galleryExtensions; + if (!rawExtension || !rawExtension.versions.length) { + return null; } - } - return null; - }); + if (version) { + const versionAsset = rawExtension.versions.filter(v => v.version === version)[0]; + if (versionAsset) { + const extension = toExtension(rawExtension, versionAsset, 0, query); + if (extension.properties.engine && isEngineValid(extension.properties.engine)) { + return extension; + } + } + return null; + } + return this.getLastValidExtensionVersion(rawExtension, rawExtension.versions) + .then(rawVersion => { + if (rawVersion) { + return toExtension(rawExtension, rawVersion, 0, query); + } + return null; + }); + }); } query(options: IQueryOptions = {}): Promise> { @@ -604,59 +624,6 @@ export class ExtensionGalleryService implements IExtensionGalleryService { }); } - loadCompatibleVersion(extension: IGalleryExtension, fromVersion: string = extension.version): Promise { - if (extension.version === fromVersion && extension.properties.engine && isEngineValid(extension.properties.engine)) { - return Promise.resolve(extension); - } - const query = new Query() - .withFlags(Flags.IncludeVersions, Flags.IncludeFiles, Flags.IncludeVersionProperties, Flags.ExcludeNonValidated) - .withPage(1, 1) - .withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code') - .withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished)) - .withAssetTypes(AssetType.Manifest, AssetType.VSIX) - .withFilter(FilterType.ExtensionId, extension.identifier.uuid); - - return this.queryGallery(query, CancellationToken.None) - .then(({ galleryExtensions }) => { - const [rawExtension] = galleryExtensions; - - if (!rawExtension) { - return null; - } - - const versions: IRawGalleryExtensionVersion[] = this.getVersionsFrom(rawExtension.versions, fromVersion); - if (!versions.length) { - return null; - } - - return this.getLastValidExtensionVersion(rawExtension, versions) - .then(rawVersion => { - if (rawVersion) { - return toExtension(rawExtension, rawVersion, 0, query); - } - return null; - }); - }); - } - - private getVersionsFrom(versions: IRawGalleryExtensionVersion[], version: string): IRawGalleryExtensionVersion[] { - if (versions[0].version === version) { - return versions; - } - const result: IRawGalleryExtensionVersion[] = []; - let currentVersion: IRawGalleryExtensionVersion | null = null; - for (const v of versions) { - if (!currentVersion) { - if (v.version === version) { - currentVersion = v; - } - } - if (currentVersion) { - result.push(v); - } - } - return result; - } private loadDependencies(extensionNames: string[], token: CancellationToken): Promise { if (!extensionNames || extensionNames.length === 0) { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 4d8a6db6c3c..ab6982abed3 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -24,7 +24,7 @@ import { import { areSameExtensions, getGalleryExtensionId, groupByExtension, getMaliciousExtensionsSet, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { localizeManifest } from '../common/extensionNls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { Limiter, always, createCancelablePromise, CancelablePromise, Queue } from 'vs/base/common/async'; +import { Limiter, createCancelablePromise, CancelablePromise, Queue } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import * as semver from 'semver'; import { URI } from 'vs/base/common/uri'; @@ -45,6 +45,8 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { IExtensionManifest, ExtensionType, ExtensionIdentifierWithVersion } from 'vs/platform/extensions/common/extensions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { isUIExtension } from 'vs/platform/extensions/node/extensionsUtil'; const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem'; const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser'; @@ -127,7 +129,9 @@ export class ExtensionManagementService extends Disposable implements IExtension onDidUninstallExtension: Event = this._onDidUninstallExtension.event; constructor( + private readonly remote: boolean, @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @ILogService private readonly logService: ILogService, @optional(IDownloadService) private downloadService: IDownloadService, @@ -302,7 +306,7 @@ export class ExtensionManagementService extends Disposable implements IExtension this.downloadInstallableExtension(extension, operation) .then(installableExtension => this.installExtension(installableExtension, ExtensionType.User, cancellationToken) - .then(local => always(pfs.rimraf(installableExtension.zipPath), () => null).then(() => local))) + .then(local => pfs.rimraf(installableExtension.zipPath).finally(() => null).then(() => local))) .then(local => this.installDependenciesAndPackExtensions(local, existingExtension) .then(() => local, error => this.uninstall(local, true).then(() => Promise.reject(error), () => Promise.reject(error)))) .then( @@ -333,12 +337,19 @@ export class ExtensionManagementService extends Disposable implements IExtension return Promise.reject(new ExtensionManagementError(nls.localize('malicious extension', "Can't install extension since it was reported to be problematic."), INSTALL_ERROR_MALICIOUS)); } - const compatibleExtension = await this.galleryService.loadCompatibleVersion(extension); + const compatibleExtension = await this.galleryService.getCompatibleExtension(extension); if (!compatibleExtension) { return Promise.reject(new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Unable to install because, the extension '{0}' compatible with current version '{1}' of VS Code is not found.", extension.identifier.id, pkg.version), INSTALL_ERROR_INCOMPATIBLE)); } + if (this.remote) { + const manifest = await this.galleryService.getManifest(extension, CancellationToken.None); + if (manifest && isUIExtension(manifest, this.configurationService)) { + return Promise.reject(new Error(nls.localize('notSupportedUIExtension', "Can't install extension {0} since UI Extensions are not supported", extension.identifier.id))); + } + } + return compatibleExtension; } @@ -453,7 +464,7 @@ export class ExtensionManagementService extends Disposable implements IExtension () => this.logService.info('Renamed to', renamePath), e => { this.logService.info('Rename failed. Deleting from extracted location', extractPath); - return always(pfs.rimraf(extractPath), () => null).then(() => Promise.reject(e)); + return pfs.rimraf(extractPath).finally(() => null).then(() => Promise.reject(e)); })); } @@ -464,7 +475,7 @@ export class ExtensionManagementService extends Disposable implements IExtension () => extract(zipPath, extractPath, { sourcePath: 'extension', overwrite: true }, this.logService, token) .then( () => this.logService.info(`Extracted extension to ${extractPath}:`, identifier.id), - e => always(pfs.rimraf(extractPath), () => null) + e => pfs.rimraf(extractPath).finally(() => null) .then(() => Promise.reject(new ExtensionManagementError(e.message, e instanceof ExtractError && e.type ? e.type : INSTALL_ERROR_EXTRACTING)))), e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING))); } @@ -480,7 +491,7 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } - private installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension | null): Promise { + private async installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension | null): Promise { if (this.galleryService.isEnabled()) { const dependenciesAndPackExtensions: string[] = installed.manifest.extensionDependencies || []; if (installed.manifest.extensionPack) { @@ -502,7 +513,16 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length }) .then(galleryResult => { const extensionsToInstall = galleryResult.firstPage; - return Promise.all(extensionsToInstall.map(e => this.installFromGallery(e))) + return Promise.all(extensionsToInstall.map(async e => { + if (this.remote) { + const manifest = await this.galleryService.getManifest(e, CancellationToken.None); + if (manifest && isUIExtension(manifest, this.configurationService)) { + this.logService.info('Ignored installing the UI dependency', e.identifier.id); + return; + } + } + return this.installFromGallery(e); + })) .then(() => null, errors => this.rollback(extensionsToInstall).then(() => Promise.reject(errors), () => Promise.reject(errors))); }); } diff --git a/src/vs/platform/extensionManagement/node/multiExtensionManagement.ts b/src/vs/platform/extensionManagement/node/multiExtensionManagement.ts index 3cd5f0acc66..f5764c324d9 100644 --- a/src/vs/platform/extensionManagement/node/multiExtensionManagement.ts +++ b/src/vs/platform/extensionManagement/node/multiExtensionManagement.ts @@ -9,7 +9,7 @@ import { IExtensionManagementServerService, IExtensionManagementServer, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { flatten } from 'vs/base/common/arrays'; -import { isUIExtension, ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; import { Disposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -17,6 +17,9 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IRemoteAuthorityResolverService, ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { ILogService } from 'vs/platform/log/common/log'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { localize } from 'vs/nls'; +import { isUIExtension } from 'vs/platform/extensions/node/extensionsUtil'; export class MultiExtensionManagementService extends Disposable implements IExtensionManagementService { @@ -50,12 +53,57 @@ export class MultiExtensionManagementService extends Disposable implements IExte .then(result => flatten(result)); } - uninstall(extension: ILocalExtension, force?: boolean): Promise { - const server = this.getServer(extension); - if (server) { - return server.extensionManagementService.uninstall(extension, force); + async uninstall(extension: ILocalExtension, force?: boolean): Promise { + if (this.extensionManagementServerService.remoteExtensionManagementServer) { + const server = this.getServer(extension); + if (!server) { + return Promise.reject(`Invalid location ${extension.location.toString()}`); + } + const syncExtensions = await this.hasToSyncExtensions(); + return syncExtensions ? this.uninstallEverywhere(extension, force) : this.uninstallInServer(extension, server, force); } - return Promise.reject(`Invalid location ${extension.location.toString()}`); + return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.uninstall(extension, force); + } + + private async uninstallEverywhere(extension: ILocalExtension, force?: boolean): Promise { + const server = this.getServer(extension); + if (!server) { + return Promise.reject(`Invalid location ${extension.location.toString()}`); + } + const promise = server.extensionManagementService.uninstall(extension); + const anotherServer: IExtensionManagementServer = server === this.extensionManagementServerService.localExtensionManagementServer ? this.extensionManagementServerService.remoteExtensionManagementServer! : this.extensionManagementServerService.localExtensionManagementServer; + const installed = await anotherServer.extensionManagementService.getInstalled(ExtensionType.User); + extension = installed.filter(i => areSameExtensions(i.identifier, extension.identifier))[0]; + if (extension) { + await anotherServer.extensionManagementService.uninstall(extension); + } + return promise; + } + + private async uninstallInServer(extension: ILocalExtension, server: IExtensionManagementServer, force?: boolean): Promise { + if (server === this.extensionManagementServerService.localExtensionManagementServer) { + const installedExtensions = await this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.getInstalled(ExtensionType.User); + const dependentNonUIExtensions = installedExtensions.filter(i => !isUIExtension(i.manifest, this.configurationService) + && i.manifest.extensionDependencies && i.manifest.extensionDependencies.some(id => areSameExtensions({ id }, extension.identifier))); + if (dependentNonUIExtensions.length) { + return Promise.reject(new Error(this.getDependentsErrorMessage(extension, dependentNonUIExtensions))); + } + } + return server.extensionManagementService.uninstall(extension, force); + } + + private getDependentsErrorMessage(extension: ILocalExtension, dependents: ILocalExtension[]): string { + if (dependents.length === 1) { + return localize('singleDependentError', "Cannot uninstall extension '{0}'. Extension '{1}' depends on this.", + extension.manifest.displayName || extension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name); + } + if (dependents.length === 2) { + return localize('twoDependentsError', "Cannot uninstall extension '{0}'. Extensions '{1}' and '{2}' depend on this.", + extension.manifest.displayName || extension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name); + } + return localize('multipleDependentsError', "Cannot uninstall extension '{0}'. Extensions '{1}', '{2}' and others depend on this.", + extension.manifest.displayName || extension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name); + } reinstallFromGallery(extension: ILocalExtension): Promise { @@ -82,35 +130,67 @@ export class MultiExtensionManagementService extends Disposable implements IExte return Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.unzip(zipLocation, type))).then(([extensionIdentifier]) => extensionIdentifier); } - install(vsix: URI): Promise { + async install(vsix: URI): Promise { if (this.extensionManagementServerService.remoteExtensionManagementServer) { - return Promise.all([getManifest(vsix.fsPath), this.hasToSyncExtensions()]) - .then(([manifest, syncExtensions]) => { - const servers = isUIExtension(manifest, this.configurationService) ? [this.extensionManagementServerService.localExtensionManagementServer] : syncExtensions ? this.servers : [this.extensionManagementServerService.remoteExtensionManagementServer!]; - return Promise.all(servers.map(server => server.extensionManagementService.install(vsix))) - .then(([extensionIdentifier]) => extensionIdentifier); - }); + const syncExtensions = await this.hasToSyncExtensions(); + if (syncExtensions) { + // Install on both servers + const [extensionIdentifier] = await Promise.all(this.servers.map(server => server.extensionManagementService.install(vsix))); + return extensionIdentifier; + } + const manifest = await getManifest(vsix.fsPath); + if (isUIExtension(manifest, this.configurationService)) { + // Install only on local server + return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.install(vsix); + } + // Install only on remote server + const promise = this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.install(vsix); + // Install UI Dependencies on local server + await this.installUIDependencies(manifest); + return promise; } return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.install(vsix); } - installFromGallery(gallery: IGalleryExtension): Promise { + async installFromGallery(gallery: IGalleryExtension): Promise { if (this.extensionManagementServerService.remoteExtensionManagementServer) { - return Promise.all([this.extensionGalleryService.getManifest(gallery, CancellationToken.None), this.hasToSyncExtensions()]) - .then(([manifest, syncExtensions]) => { - if (manifest) { - const servers = manifest && isUIExtension(manifest, this.configurationService) ? [this.extensionManagementServerService.localExtensionManagementServer] : syncExtensions ? this.servers : [this.extensionManagementServerService.remoteExtensionManagementServer!]; - return Promise.all(servers.map(server => server.extensionManagementService.installFromGallery(gallery))) - .then(() => undefined); - } else { - this.logService.info('Manifest was not found. Hence installing only in local server'); - return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery); - } - }); + const [manifest, syncExtensions] = await Promise.all([this.extensionGalleryService.getManifest(gallery, CancellationToken.None), this.hasToSyncExtensions()]); + if (manifest) { + if (syncExtensions) { + // Install on both servers + return Promise.all(this.servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(() => undefined); + } + if (isUIExtension(manifest, this.configurationService)) { + // Install only on local server + return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery); + } + // Install only on remote server + const promise = this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(gallery); + // Install UI Dependencies on local server + await this.installUIDependencies(manifest); + return promise; + } else { + this.logService.info('Manifest was not found. Hence installing only in local server'); + return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery); + } } return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery); } + private async installUIDependencies(manifest: IExtensionManifest): Promise { + if (manifest.extensionDependencies && manifest.extensionDependencies.length) { + const dependencies = await this.extensionGalleryService.loadAllDependencies(manifest.extensionDependencies.map(id => ({ id })), CancellationToken.None); + if (dependencies.length) { + await Promise.all(dependencies.map(async d => { + const manifest = await this.extensionGalleryService.getManifest(d, CancellationToken.None); + if (manifest && isUIExtension(manifest, this.configurationService)) { + await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(d); + } + })); + } + } + } + getExtensionsReport(): Promise { return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.getExtensionsReport(); } diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index ec9209e929f..1fc3e1913be 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -3,10 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { getGalleryExtensionId, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import * as strings from 'vs/base/common/strings'; -import { isNonEmptyArray } from 'vs/base/common/arrays'; import { ILocalization } from 'vs/platform/localizations/common/localizations'; import { URI } from 'vs/base/common/uri'; @@ -129,6 +127,13 @@ export class ExtensionIdentifierWithVersion { } } +export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier { + return thing + && typeof thing === 'object' + && typeof thing.id === 'string' + && (!thing.uuid || typeof thing.uuid === 'string'); +} + export interface IExtensionIdentifier { id: string; uuid?: string; @@ -167,39 +172,6 @@ export interface IExtension { readonly location: URI; } -const uiExtensions = new Set(); -uiExtensions.add('msjsdiag.debugger-for-chrome'); - -export function isUIExtension(manifest: IExtensionManifest, configurationService: IConfigurationService): boolean { - const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); - const configuredUIExtensions = configurationService.getValue('_workbench.uiExtensions') || []; - if (configuredUIExtensions.length) { - if (configuredUIExtensions.indexOf(extensionId) !== -1) { - return true; - } - if (configuredUIExtensions.indexOf(`-${extensionId}`) !== -1) { - return false; - } - } - switch (manifest.extensionKind) { - case 'ui': return true; - case 'workspace': return false; - default: { - if (uiExtensions.has(extensionId)) { - return true; - } - if (manifest.main) { - return false; - } - if (manifest.contributes && isNonEmptyArray(manifest.contributes.debuggers)) { - return false; - } - // Default is UI Extension - return true; - } - } -} - /** * **!Do not construct directly!** * diff --git a/src/vs/platform/extensions/node/extensionsUtil.ts b/src/vs/platform/extensions/node/extensionsUtil.ts new file mode 100644 index 00000000000..5041bf9577e --- /dev/null +++ b/src/vs/platform/extensions/node/extensionsUtil.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { getGalleryExtensionId, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; +import product from 'vs/platform/node/product'; + +export function isUIExtension(manifest: IExtensionManifest, configurationService: IConfigurationService): boolean { + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); + const configuredUIExtensions = configurationService.getValue('_workbench.uiExtensions') || []; + if (configuredUIExtensions.length) { + if (configuredUIExtensions.indexOf(extensionId) !== -1) { + return true; + } + if (configuredUIExtensions.indexOf(`-${extensionId}`) !== -1) { + return false; + } + } + switch (manifest.extensionKind) { + case 'ui': return true; + case 'workspace': return false; + default: { + if (isNonEmptyArray(product.uiExtensions) && product.uiExtensions.some(id => areSameExtensions({ id }, { id: extensionId }))) { + return true; + } + if (manifest.main) { + return false; + } + if (manifest.contributes && isNonEmptyArray(manifest.contributes.debuggers)) { + return false; + } + // Default is UI Extension + return true; + } + } +} diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index b007b6f1840..fce0e320b86 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -168,6 +168,10 @@ export interface FileWriteOptions { create: boolean; } +export interface FileOpenOptions { + create: boolean; +} + export interface FileDeleteOptions { recursive: boolean; } @@ -219,7 +223,7 @@ export interface IFileSystemProvider { readFile?(resource: URI): Promise; writeFile?(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise; - open?(resource: URI): Promise; + open?(resource: URI, opts: FileOpenOptions): Promise; close?(fd: number): Promise; read?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise; write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise; diff --git a/src/vs/platform/history/electron-main/historyMainService.ts b/src/vs/platform/history/electron-main/historyMainService.ts index 85ecbea3aed..1b0d20dc036 100644 --- a/src/vs/platform/history/electron-main/historyMainService.ts +++ b/src/vs/platform/history/electron-main/historyMainService.ts @@ -12,7 +12,7 @@ import { getBaseLabel, getPathLabel } from 'vs/base/common/labels'; import { IPath } from 'vs/platform/windows/common/windows'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import { isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier, IWorkspacesMainService, IWorkspaceSavedEvent, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IWorkspacesMainService, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IHistoryMainService, IRecentlyOpened } from 'vs/platform/history/common/history'; import { isEqual } from 'vs/base/common/paths'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -54,18 +54,6 @@ export class HistoryMainService implements IHistoryMainService { @IEnvironmentService private readonly environmentService: IEnvironmentService ) { this.macOSRecentDocumentsUpdater = new RunOnceScheduler(() => this.updateMacOSRecentDocuments(), 800); - - this.registerListeners(); - } - - private registerListeners(): void { - this.workspacesMainService.onWorkspaceSaved(e => this.onWorkspaceSaved(e)); - } - - private onWorkspaceSaved(e: IWorkspaceSavedEvent): void { - - // Make sure to add newly saved workspaces to the list of recent workspaces - this.addRecentlyOpened([e.workspace], []); } addRecentlyOpened(workspaces: Array, files: URI[]): void { diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts index a396d295560..28697f4c745 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts @@ -12,7 +12,6 @@ import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { Disposable } from 'vs/base/common/lifecycle'; -import { always } from 'vs/base/common/async'; export const ILifecycleService = createDecorator('lifecycleService'); @@ -202,7 +201,7 @@ export class LifecycleService extends Disposable implements ILifecycleService { const shutdownPromise = this.beginOnWillShutdown(); // Wait until shutdown is signaled to be complete - always(shutdownPromise, () => { + shutdownPromise.finally(() => { // Resolve pending quit promise now without veto this.resolvePendingQuitPromise(false /* no veto */); diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index abf900f8577..515329e5de1 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -217,6 +217,7 @@ function handleTreeController(configuration: ITreeConfiguration, instantiationSe export class WorkbenchList extends List { readonly contextKeyService: IContextKeyService; + private readonly configurationService: IConfigurationService; private listHasSelectionOrFocus: IContextKey; private listDoubleSelection: IContextKey; @@ -232,19 +233,23 @@ export class WorkbenchList extends List { @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, - @IConfigurationService private readonly configurationService: IConfigurationService, + @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService ) { + const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); + super(container, delegate, renderers, { keyboardSupport: false, styleController: new DefaultStyleController(getSharedListStyleSheet()), ...computeStyles(themeService.getTheme(), defaultListStyles), - ...toWorkbenchListOptions(options, configurationService, keybindingService) + ...toWorkbenchListOptions(options, configurationService, keybindingService), + horizontalScrolling } as IListOptions ); this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + this.configurationService = configurationService; const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false)); @@ -294,8 +299,9 @@ export class WorkbenchList extends List { export class WorkbenchPagedList extends PagedList { readonly contextKeyService: IContextKeyService; + private readonly configurationService: IConfigurationService; - private disposables: IDisposable[] = []; + private disposables: IDisposable[]; private _useAltAsMultipleSelectionModifier: boolean; @@ -307,19 +313,24 @@ export class WorkbenchPagedList extends PagedList { @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, - @IConfigurationService private readonly configurationService: IConfigurationService, + @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService ) { + const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); super(container, delegate, renderers, { keyboardSupport: false, styleController: new DefaultStyleController(getSharedListStyleSheet()), ...computeStyles(themeService.getTheme(), defaultListStyles), - ...toWorkbenchListOptions(options, configurationService, keybindingService) + ...toWorkbenchListOptions(options, configurationService, keybindingService), + horizontalScrolling } as IListOptions ); + this.disposables = []; + this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + this.configurationService = configurationService; const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false)); @@ -626,7 +637,7 @@ export class TreeResourceNavigator2 extends Disposable { } private onSelection(e: ITreeEvent): void { - if (!e.browserEvent) { + if (!e.browserEvent || e.browserEvent.type === 'contextmenu') { return; } @@ -907,6 +918,7 @@ export class WorkbenchObjectTree, TFilterData = void> @IKeybindingService keybindingService: IKeybindingService ) { const keyboardNavigation = configurationService.getValue(keyboardNavigationSettingKey); + const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); super(container, delegate, renderers, { keyboardSupport: false, @@ -915,7 +927,8 @@ export class WorkbenchObjectTree, TFilterData = void> ...toWorkbenchListOptions(options, configurationService, keybindingService), indent: configurationService.getValue(treeIndentKey), simpleKeyboardNavigation: keyboardNavigation === 'simple', - filterOnType: keyboardNavigation === 'filter' + filterOnType: keyboardNavigation === 'filter', + horizontalScrolling }); this.contextKeyService = createScopedContextKeyService(contextKeyService, this); @@ -999,6 +1012,7 @@ export class WorkbenchDataTree extends DataTree(keyboardNavigationSettingKey); + const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); super(container, delegate, renderers, dataSource, { keyboardSupport: false, @@ -1007,7 +1021,8 @@ export class WorkbenchDataTree extends DataTree extends Async @IKeybindingService keybindingService: IKeybindingService ) { const keyboardNavigation = configurationService.getValue(keyboardNavigationSettingKey); + const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); super(container, delegate, renderers, dataSource, { keyboardSupport: false, @@ -1094,7 +1110,8 @@ export class WorkbenchAsyncDataTree extends Async ...toWorkbenchListOptions(options, configurationService, keybindingService), indent: configurationService.getValue(treeIndentKey), simpleKeyboardNavigation: keyboardNavigation === 'simple', - filterOnType: keyboardNavigation === 'filter' + filterOnType: keyboardNavigation === 'filter', + horizontalScrolling }); this.contextKeyService = createScopedContextKeyService(contextKeyService, this); @@ -1186,7 +1203,7 @@ configurationRegistry.registerConfiguration({ [horizontalScrollingKey]: { 'type': 'boolean', 'default': false, - 'description': localize('horizontalScrolling setting', "Controls whether trees support horizontal scrolling in the workbench.") + 'description': localize('horizontalScrolling setting', "Controls whether lists and trees support horizontal scrolling in the workbench.") }, [treeIndentKey]: { 'type': 'number', @@ -1197,12 +1214,12 @@ configurationRegistry.registerConfiguration({ 'type': 'string', 'enum': ['simple', 'highlight', 'filter'], 'enumDescriptions': [ - localize('keyboardNavigationSettingKey.simple', "Sets simple keyboard navigation for lists and trees."), - localize('keyboardNavigationSettingKey.highlight', "Enables highlighting keyboard navigation for lists and trees."), - localize('keyboardNavigationSettingKey.filter', "Enables filtering keyboard navigation for lists and trees.") + localize('keyboardNavigationSettingKey.simple', "Simple keyboard navigation focuses elements which match the keyboard input. Matching is done only on prefixes."), + localize('keyboardNavigationSettingKey.highlight', "Highlight keyboard navigation highlights elements which match the keyboard input. Further up and down navigation will traverse only the highlighted elements."), + localize('keyboardNavigationSettingKey.filter', "Filter keyboard navigation will filter out and hide all the elements which do not match the keyboard input.") ], 'default': 'highlight', - 'description': localize('keyboardNavigationSettingKey', "Controls the keyboard navigation style for lists and trees.") + 'description': localize('keyboardNavigationSettingKey', "Controls the keyboard navigation style for lists and trees in the workbench. Can be simple, highlight and filter.") }, } }); diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index b249609a84f..7bda195c819 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -709,14 +709,15 @@ export class Menubar { if (activeWindow) { if (invocation.type === 'commandId') { - if (!activeWindow.isReady && isMacintosh && invocation.commandId === 'workbench.action.toggleDevTools' && !this.environmentService.isBuilt) { + this.windowsMainService.sendToFocused('vscode:runAction', { id: invocation.commandId, from: 'menu' } as IRunActionInWindowRequest); + } else { + if (isMacintosh && invocation.userSettingsLabel === 'alt+cmd+i' && !this.environmentService.isBuilt && !activeWindow.isReady) { // prevent this action from running twice on macOS (https://github.com/Microsoft/vscode/issues/62719) // we already register a keybinding in bootstrap-window.js for opening developer tools in case something // goes wrong and that keybinding is only removed when the application has loaded (= window ready). return; } - this.windowsMainService.sendToFocused('vscode:runAction', { id: invocation.commandId, from: 'menu' } as IRunActionInWindowRequest); - } else { + this.windowsMainService.sendToFocused('vscode:runKeybinding', { userSettingsLabel: invocation.userSettingsLabel } as IRunKeybindingInWindowRequest); } } diff --git a/src/vs/platform/node/product.ts b/src/vs/platform/node/product.ts index dbb84fd473f..08b8b607e2b 100644 --- a/src/vs/platform/node/product.ts +++ b/src/vs/platform/node/product.ts @@ -80,6 +80,7 @@ export interface IProductConfiguration { }; logUploaderUrl: string; portable?: string; + uiExtensions?: string[]; } export interface ISurveyData { diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 37f2d1630cf..8a82a1eb30c 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -52,7 +52,7 @@ export interface IProgressService2 { _serviceBrand: any; - withProgress

, R=any>(options: IProgressOptions, task: (progress: IProgress) => P, onDidCancel?: () => void): P; + withProgress(options: IProgressOptions, task: (progress: IProgress) => Promise, onDidCancel?: () => void): Promise; } export interface IProgressRunner { diff --git a/src/vs/platform/search/common/search.ts b/src/vs/platform/search/common/search.ts index 39029dc6706..877b69da34a 100644 --- a/src/vs/platform/search/common/search.ts +++ b/src/vs/platform/search/common/search.ts @@ -81,7 +81,6 @@ export interface ICommonQueryProps { excludePattern?: glob.IExpression; extraFileResources?: U[]; - useRipgrep?: boolean; maxResults?: number; usingSearchPaths?: boolean; } @@ -232,7 +231,6 @@ export interface ICachedSearchStats { } export interface ISearchEngineStats { - traversal: string; fileWalkTime: number; directoriesWalked: number; filesWalked: number; @@ -317,7 +315,6 @@ export class OneLineRange extends SearchRange { export interface ISearchConfigurationProperties { exclude: glob.IExpression; useRipgrep: boolean; - useLegacySearch: boolean; /** * Use ignore file for file search. */ diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index 54d0aeb6fe0..02513305828 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -67,8 +67,8 @@ export interface IStorageService { * The scope argument allows to define the scope of the storage * operation to either the current workspace only or all workspaces. */ - getInteger(key: string, scope: StorageScope, fallbackValue: number): number; - getInteger(key: string, scope: StorageScope, fallbackValue?: number): number | undefined; + getInteger(key: string, scope: StorageScope, fallbackValue: number): number; + getInteger(key: string, scope: StorageScope, fallbackValue?: number): number | undefined; /** * Store a value under the given key to storage. The value will be converted to a string. diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts index 886049f1b15..40749376be2 100644 --- a/src/vs/platform/storage/node/storageIpc.ts +++ b/src/vs/platform/storage/node/storageIpc.ts @@ -57,7 +57,7 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC private serializeEvents(events: IStorageChangeEvent[]): ISerializableItemsChangeEvent { const items = new Map(); - events.forEach(event => items.set(event.key, this.storageMainService.get(event.key, ''))); + events.forEach(event => items.set(event.key, this.storageMainService.get(event.key))); return { items: mapToSerializable(items) } as ISerializableItemsChangeEvent; } diff --git a/src/vs/platform/storage/node/storageMainService.ts b/src/vs/platform/storage/node/storageMainService.ts index f8f5d6be928..71f7299951f 100644 --- a/src/vs/platform/storage/node/storageMainService.ts +++ b/src/vs/platform/storage/node/storageMainService.ts @@ -39,6 +39,7 @@ export interface IStorageMainService { * the provided defaultValue if the element is null or undefined. */ get(key: string, fallbackValue: string): string; + get(key: string, fallbackValue?: string): string | undefined; /** * Retrieve an element stored with the given key from storage. Use @@ -46,6 +47,7 @@ export interface IStorageMainService { * will be converted to a boolean. */ getBoolean(key: string, fallbackValue: boolean): boolean; + getBoolean(key: string, fallbackValue?: boolean): boolean | undefined; /** * Retrieve an element stored with the given key from storage. Use @@ -53,6 +55,7 @@ export interface IStorageMainService { * will be converted to a number using parseInt with a base of 10. */ getInteger(key: string, fallbackValue: number): number; + getInteger(key: string, fallbackValue?: number): number | undefined; /** * Store a string value under the given key to storage. The value will @@ -345,15 +348,21 @@ export class StorageMainService extends Disposable implements IStorageMainServic }); } - get(key: string, fallbackValue: string): string { + get(key: string, fallbackValue: string): string; + get(key: string, fallbackValue?: string): string | undefined; + get(key: string, fallbackValue?: string): string | undefined { return this.storage.get(key, fallbackValue); } - getBoolean(key: string, fallbackValue: boolean): boolean { + getBoolean(key: string, fallbackValue: boolean): boolean; + getBoolean(key: string, fallbackValue?: boolean): boolean | undefined; + getBoolean(key: string, fallbackValue?: boolean): boolean | undefined { return this.storage.getBoolean(key, fallbackValue); } - getInteger(key: string, fallbackValue: number): number { + getInteger(key: string, fallbackValue: number): number; + getInteger(key: string, fallbackValue?: number): number | undefined; + getInteger(key: string, fallbackValue?: number): number | undefined { return this.storage.getInteger(key, fallbackValue); } diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index f14f28c3f5c..a1f3ee581d6 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -225,6 +225,7 @@ export interface IListStyleOverrides extends IStyleOverrides { listHoverOutline?: ColorIdentifier; listMatchesBackground?: ColorIdentifier; listMatchesOutline?: ColorIdentifier; + listNoMatchesOutline?: ColorIdentifier; listMatchesShadow?: ColorIdentifier; } @@ -249,6 +250,7 @@ export const defaultListStyles: IColorMapping = { listSelectionOutline: activeContrastBorder, listHoverOutline: activeContrastBorder, listMatchesBackground: blend2(editorFindMatchHighlight, editorWidgetBackground), + listNoMatchesOutline: inputValidationWarningBorder, listMatchesShadow: widgetShadow }; diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index a93657345c6..a1afe880e9a 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -125,9 +125,11 @@ export class LinuxUpdateService extends AbstractUpdateService { } // Allow 3 seconds for VS Code to close - spawn('bash', ['-c', path.join(snap, `usr/share/${product.applicationName}/snapUpdate.sh`)], { + spawn('sleep 3 && $SNAP_NAME', { + shell: true, detached: true, - stdio: ['ignore', 'ignore', 'ignore'] + stdio: 'ignore', }); + } } diff --git a/src/vs/platform/update/electron-main/updateService.snap.ts b/src/vs/platform/update/electron-main/updateService.snap.ts index 74e5d430a43..bcbb7f22817 100644 --- a/src/vs/platform/update/electron-main/updateService.snap.ts +++ b/src/vs/platform/update/electron-main/updateService.snap.ts @@ -6,7 +6,6 @@ import { Event, Emitter } from 'vs/base/common/event'; import { timeout } from 'vs/base/common/async'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; -import product from 'vs/platform/node/product'; import { IUpdateService, State, StateType, AvailableForDownload, UpdateType } from 'vs/platform/update/common/update'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; @@ -14,7 +13,6 @@ import * as path from 'path'; import { realpath, watch } from 'fs'; import { spawn } from 'child_process'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { stat } from 'vs/base/node/pfs'; abstract class AbstractUpdateService2 implements IUpdateService { @@ -137,8 +135,6 @@ export class SnapUpdateService extends AbstractUpdateService2 { _serviceBrand: any; - private snapUpdatePath: string; - constructor( private snap: string, private snapRevision: string, @@ -149,8 +145,6 @@ export class SnapUpdateService extends AbstractUpdateService2 { ) { super(lifecycleService, environmentService, logService); - this.snapUpdatePath = path.join(this.snap, `usr/share/${product.applicationName}/snapUpdate.sh`); - const watcher = watch(path.dirname(this.snap)); const onChange = Event.fromNodeEventEmitter(watcher, 'change', (_, fileName: string) => fileName); const onCurrentChange = Event.filter(onChange, n => n === 'current'); @@ -196,19 +190,14 @@ export class SnapUpdateService extends AbstractUpdateService2 { this.logService.trace('update#quitAndInstall(): running raw#quitAndInstall()'); // Allow 3 seconds for VS Code to close - spawn('bash', ['-c', this.snapUpdatePath], { + spawn('sleep 3 && $SNAP_NAME', { + shell: true, detached: true, - stdio: ['ignore', 'ignore', 'ignore'] + stdio: 'ignore', }); } private async isUpdateAvailable(): Promise { - try { - await stat(this.snapUpdatePath); - } catch (err) { - return false; - } - const resolvedCurrentSnapPath = await new Promise((c, e) => realpath(`${path.dirname(this.snap)}/current`, (err, r) => err ? e(err) : c(r))); const currentRevision = path.basename(resolvedCurrentSnapPath); return this.snapRevision !== currentRevision; diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 2d1761c2446..56eaeef6f49 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -6,9 +6,9 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; -import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IRecentlyOpened } from 'vs/platform/history/common/history'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { ExportData } from 'vs/base/common/performance'; @@ -114,9 +114,7 @@ export interface IWindowsService { openDevTools(windowId: number, options?: IDevToolsOptions): Promise; toggleDevTools(windowId: number): Promise; closeWorkspace(windowId: number): Promise; - enterWorkspace(windowId: number, path: string): Promise; - createAndEnterWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], path?: string): Promise; - saveAndEnterWorkspace(windowId: number, path: string): Promise; + enterWorkspace(windowId: number, path: URI): Promise; toggleFullScreen(windowId: number): Promise; setRepresentedFilename(windowId: number, fileName: string): Promise; addRecentlyOpened(files: URI[]): Promise; @@ -207,9 +205,7 @@ export interface IWindowService { toggleDevTools(): Promise; closeWorkspace(): Promise; updateTouchBar(items: ISerializableCommandAction[][]): Promise; - enterWorkspace(path: string): Promise; - createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): Promise; - saveAndEnterWorkspace(path: string): Promise; + enterWorkspace(path: URI): Promise; toggleFullScreen(): Promise; setRepresentedFilename(fileName: string): Promise; getRecentlyOpened(): Promise; @@ -251,7 +247,6 @@ export interface IWindowSettings { nativeFullScreen: boolean; enableMenuBarMnemonics: boolean; closeWhenEmpty: boolean; - smoothScrollingWorkaround: boolean; clickThroughInactive: boolean; } @@ -274,11 +269,6 @@ export function getTitleBarStyle(configurationService: IConfigurationService, en return 'native'; // simple fullscreen does not work well with custom title style (https://github.com/Microsoft/vscode/issues/63291) } - const smoothScrollingWorkaround = isWindows && configuration.smoothScrollingWorkaround === true; - if (smoothScrollingWorkaround) { - return 'native'; // smooth scrolling workaround does not work with custom title style - } - const style = configuration.titleBarStyle; if (style === 'native') { return 'native'; diff --git a/src/vs/platform/windows/electron-browser/windowService.ts b/src/vs/platform/windows/electron-browser/windowService.ts index 60457efd10c..9c9cc78d419 100644 --- a/src/vs/platform/windows/electron-browser/windowService.ts +++ b/src/vs/platform/windows/electron-browser/windowService.ts @@ -7,7 +7,6 @@ import { Event } from 'vs/base/common/event'; import { IWindowService, IWindowsService, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IWindowConfiguration, IDevToolsOptions, IOpenSettings } from 'vs/platform/windows/common/windows'; import { IRecentlyOpened } from 'vs/platform/history/common/history'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; -import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; import { URI } from 'vs/base/common/uri'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -89,18 +88,10 @@ export class WindowService extends Disposable implements IWindowService { return this.windowsService.closeWorkspace(this.windowId); } - enterWorkspace(path: string): Promise { + enterWorkspace(path: URI): Promise { return this.windowsService.enterWorkspace(this.windowId, path); } - createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): Promise { - return this.windowsService.createAndEnterWorkspace(this.windowId, folders, path); - } - - saveAndEnterWorkspace(path: string): Promise { - return this.windowsService.saveAndEnterWorkspace(this.windowId, path); - } - openWindow(paths: URI[], options?: IOpenSettings): Promise { return this.windowsService.openWindow(this.windowId, paths, options); } diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 4edc86ab24f..e067b62e96d 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -8,7 +8,7 @@ import { ParsedArgs } from 'vs/platform/environment/common/environment'; import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProcessEnvironment } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { URI } from 'vs/base/common/uri'; @@ -88,17 +88,13 @@ export interface IWindowsMainService { // events readonly onWindowReady: Event; - readonly onActiveWindowChanged: Event; readonly onWindowsCountChanged: Event; readonly onWindowClose: Event; - readonly onWindowReload: Event; // methods ready(initialUserEnv: IProcessEnvironment): void; reload(win: ICodeWindow, cli?: ParsedArgs): void; - enterWorkspace(win: ICodeWindow, path: string): Promise; - createAndEnterWorkspace(win: ICodeWindow, folders?: IWorkspaceFolderCreationData[], path?: string): Promise; - saveAndEnterWorkspace(win: ICodeWindow, path: string): Promise; + enterWorkspace(win: ICodeWindow, path: URI): Promise; closeWorkspace(win: ICodeWindow): void; open(openConfig: IOpenConfiguration): ICodeWindow[]; openExtensionDevelopmentHostWindow(openConfig: IOpenConfiguration): void; diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index 8e3998a11e3..940cbe2d1cd 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -17,7 +17,7 @@ import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { IWindowsMainService, ISharedProcess, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; import { IHistoryMainService, IRecentlyOpened } from 'vs/platform/history/common/history'; -import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { Schemas } from 'vs/base/common/network'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; @@ -138,24 +138,12 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return this.withWindow(windowId, codeWindow => this.windowsMainService.closeWorkspace(codeWindow)); } - async enterWorkspace(windowId: number, path: string): Promise { + async enterWorkspace(windowId: number, path: URI): Promise { this.logService.trace('windowsService#enterWorkspace', windowId); return this.withWindow(windowId, codeWindow => this.windowsMainService.enterWorkspace(codeWindow, path)); } - async createAndEnterWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], path?: string): Promise { - this.logService.trace('windowsService#createAndEnterWorkspace', windowId); - - return this.withWindow(windowId, codeWindow => this.windowsMainService.createAndEnterWorkspace(codeWindow, folders, path)); - } - - async saveAndEnterWorkspace(windowId: number, path: string): Promise { - this.logService.trace('windowsService#saveAndEnterWorkspace', windowId); - - return this.withWindow(windowId, codeWindow => this.windowsMainService.saveAndEnterWorkspace(codeWindow, path)); - } - async toggleFullScreen(windowId: number): Promise { this.logService.trace('windowsService#toggleFullScreen', windowId); diff --git a/src/vs/platform/windows/node/windowsIpc.ts b/src/vs/platform/windows/node/windowsIpc.ts index f590f6c7832..dc10702ca5b 100644 --- a/src/vs/platform/windows/node/windowsIpc.ts +++ b/src/vs/platform/windows/node/windowsIpc.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/node/ipc'; import { IWindowsService, INativeOpenDialogOptions, IEnterWorkspaceResult, CrashReporterStartOptions, IMessageBoxResult, MessageBoxOptions, SaveDialogOptions, OpenDialogOptions, IDevToolsOptions, INewWindowOptions } from 'vs/platform/windows/common/windows'; -import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IRecentlyOpened } from 'vs/platform/history/common/history'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { URI } from 'vs/base/common/uri'; @@ -56,22 +56,7 @@ export class WindowsChannel implements IServerChannel { case 'openDevTools': return this.service.openDevTools(arg[0], arg[1]); case 'toggleDevTools': return this.service.toggleDevTools(arg); case 'closeWorkspace': return this.service.closeWorkspace(arg); - case 'enterWorkspace': return this.service.enterWorkspace(arg[0], arg[1]); - case 'createAndEnterWorkspace': { - const rawFolders: IWorkspaceFolderCreationData[] = arg[1]; - let folders: IWorkspaceFolderCreationData[] | undefined = undefined; - if (Array.isArray(rawFolders)) { - folders = rawFolders.map(rawFolder => { - return { - uri: URI.revive(rawFolder.uri), // convert raw URI back to real URI - name: rawFolder.name - } as IWorkspaceFolderCreationData; - }); - } - - return this.service.createAndEnterWorkspace(arg[0], folders, arg[2]); - } - case 'saveAndEnterWorkspace': return this.service.saveAndEnterWorkspace(arg[0], arg[1]); + case 'enterWorkspace': return this.service.enterWorkspace(arg[0], URI.revive(arg[1])); case 'toggleFullScreen': return this.service.toggleFullScreen(arg); case 'setRepresentedFilename': return this.service.setRepresentedFilename(arg[0], arg[1]); case 'addRecentlyOpened': return this.service.addRecentlyOpened(arg.map(URI.revive)); @@ -179,18 +164,10 @@ export class WindowsChannelClient implements IWindowsService { return this.channel.call('closeWorkspace', windowId); } - enterWorkspace(windowId: number, path: string): Promise { + enterWorkspace(windowId: number, path: URI): Promise { return this.channel.call('enterWorkspace', [windowId, path]); } - createAndEnterWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], path?: string): Promise { - return this.channel.call('createAndEnterWorkspace', [windowId, folders, path]); - } - - saveAndEnterWorkspace(windowId: number, path: string): Promise { - return this.channel.call('saveAndEnterWorkspace', [windowId, path]); - } - toggleFullScreen(windowId: number): Promise { return this.channel.call('toggleFullScreen', windowId); } diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 8abb0ca735d..9521062c3ac 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -46,7 +46,13 @@ export interface IWorkspaceContextService { onDidChangeWorkspaceFolders: Event; /** - * Provides access to the workspace object the platform is running with. + * Provides access to the complete workspace object. + */ + getCompleteWorkspace(): Promise; + + /** + * Provides access to the workspace object the window is running with. + * Use `getCompleteWorkspace` to get complete workspace object. */ getWorkspace(): IWorkspace; diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index d913586c132..ec81bacefb9 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -77,14 +77,11 @@ export interface IWorkspaceFolderCreationData { export interface IWorkspacesMainService extends IWorkspacesService { _serviceBrand: any; - onWorkspaceSaved: Event; onUntitledWorkspaceDeleted: Event; - saveWorkspace(workspace: IWorkspaceIdentifier, target: string): Promise; + saveWorkspaceAs(workspace: IWorkspaceIdentifier, target: string): Promise; - createWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; - - resolveWorkspace(path: string): Promise; + createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; resolveWorkspaceSync(path: string): IResolvedWorkspace | null; @@ -95,12 +92,14 @@ export interface IWorkspacesMainService extends IWorkspacesService { getUntitledWorkspacesSync(): IWorkspaceIdentifier[]; getWorkspaceId(workspacePath: string): string; + + getWorkspaceIdentifier(workspacePath: URI): IWorkspaceIdentifier; } export interface IWorkspacesService { _serviceBrand: any; - createWorkspace(folders?: IWorkspaceFolderCreationData[]): Promise; + createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[]): Promise; } export function isSingleFolderWorkspaceIdentifier(obj: any): obj is ISingleFolderWorkspaceIdentifier { diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index 2828f1268ae..015156751a9 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_EXTENSION, IWorkspaceSavedEvent, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isRawFileWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_EXTENSION, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { isParent } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { extname, join, dirname, isAbsolute, resolve } from 'path'; +import { join, dirname, extname } from 'path'; import { mkdirp, writeFile, readFile } from 'vs/base/node/pfs'; import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs'; -import { isLinux, isMacintosh } from 'vs/base/common/platform'; +import { isLinux } from 'vs/base/common/platform'; import { delSync, readdirSync, writeFileAndFlushSync } from 'vs/base/node/extfs'; import { Event, Emitter } from 'vs/base/common/event'; import { ILogService } from 'vs/platform/log/common/log'; @@ -17,12 +17,12 @@ import { isEqual } from 'vs/base/common/paths'; import { coalesce } from 'vs/base/common/arrays'; import { createHash } from 'crypto'; import * as json from 'vs/base/common/json'; -import * as jsonEdit from 'vs/base/common/jsonEdit'; -import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/workspaces'; +import { massageFolderPathForWorkspace, rewriteWorkspaceFileForNewLocation } from 'vs/platform/workspaces/node/workspaces'; import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { Disposable } from 'vs/base/common/lifecycle'; +import { fsPath, dirname as resourcesDirname } from 'vs/base/common/resources'; export interface IStoredWorkspace { folders: IStoredWorkspaceFolder[]; @@ -34,9 +34,6 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain private workspacesHome: string; - private readonly _onWorkspaceSaved = this._register(new Emitter()); - get onWorkspaceSaved(): Event { return this._onWorkspaceSaved.event; } - private readonly _onUntitledWorkspaceDeleted = this._register(new Emitter()); get onUntitledWorkspaceDeleted(): Event { return this._onUntitledWorkspaceDeleted.event; } @@ -49,14 +46,6 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain this.workspacesHome = environmentService.workspacesHome; } - resolveWorkspace(path: string): Promise { - if (!this.isWorkspacePath(path)) { - return Promise.resolve(null); // does not look like a valid workspace config file - } - - return readFile(path, 'utf8').then(contents => this.doResolveWorkspace(path, contents)); - } - resolveWorkspaceSync(path: string): IResolvedWorkspace | null { if (!this.isWorkspacePath(path)) { return null; // does not look like a valid workspace config file @@ -69,21 +58,21 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain return null; // invalid workspace } - return this.doResolveWorkspace(path, contents); + return this.doResolveWorkspace(URI.file(path), contents); } private isWorkspacePath(path: string): boolean { return this.isInsideWorkspacesHome(path) || extname(path) === `.${WORKSPACE_EXTENSION}`; } - private doResolveWorkspace(path: string, contents: string): IResolvedWorkspace | null { + private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | null { try { const workspace = this.doParseStoredWorkspace(path, contents); - + const workspaceIdentifier = this.getWorkspaceIdentifier(path); return { - id: this.getWorkspaceId(path), - configPath: path, - folders: toWorkspaceFolders(workspace.folders, URI.file(dirname(path))) + id: workspaceIdentifier.id, + configPath: workspaceIdentifier.configPath, + folders: toWorkspaceFolders(workspace.folders, resourcesDirname(path)!) }; } catch (error) { this.logService.warn(error.toString()); @@ -92,7 +81,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain return null; } - private doParseStoredWorkspace(path: string, contents: string): IStoredWorkspace { + private doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { // Parse workspace file let storedWorkspace: IStoredWorkspace = json.parse(contents); // use fault tolerant parser @@ -104,7 +93,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain // Validate if (!Array.isArray(storedWorkspace.folders)) { - throw new Error(`${path} looks like an invalid workspace file.`); + throw new Error(`${path.toString()} looks like an invalid workspace file.`); } return storedWorkspace; @@ -114,16 +103,16 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain return isParent(path, this.environmentService.workspacesHome, !isLinux /* ignore case */); } - createWorkspace(folders?: IWorkspaceFolderCreationData[]): Promise { - const { workspace, configParent, storedWorkspace } = this.createUntitledWorkspace(folders); + createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[]): Promise { + const { workspace, configParent, storedWorkspace } = this.newUntitledWorkspace(folders); return mkdirp(configParent).then(() => { return writeFile(workspace.configPath, JSON.stringify(storedWorkspace, null, '\t')).then(() => workspace); }); } - createWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier { - const { workspace, configParent, storedWorkspace } = this.createUntitledWorkspace(folders); + createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier { + const { workspace, configParent, storedWorkspace } = this.newUntitledWorkspace(folders); if (!existsSync(this.workspacesHome)) { mkdirSync(this.workspacesHome); @@ -136,7 +125,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain return workspace; } - private createUntitledWorkspace(folders: IWorkspaceFolderCreationData[] = []): { workspace: IWorkspaceIdentifier, configParent: string, storedWorkspace: IStoredWorkspace } { + private newUntitledWorkspace(folders: IWorkspaceFolderCreationData[] = []): { workspace: IWorkspaceIdentifier, configParent: string, storedWorkspace: IStoredWorkspace } { const randomId = (Date.now() + Math.round(Math.random() * 1000)).toString(); const untitledWorkspaceConfigFolder = join(this.workspacesHome, randomId); const untitledWorkspaceConfigPath = join(untitledWorkspaceConfigFolder, UNTITLED_WORKSPACE_NAME); @@ -148,7 +137,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain // File URI if (folderResource.scheme === Schemas.file) { - storedWorkspace = { path: massageFolderPathForWorkspace(folderResource.fsPath, untitledWorkspaceConfigFolder, []) }; + storedWorkspace = { path: massageFolderPathForWorkspace(fsPath(folderResource), URI.file(untitledWorkspaceConfigFolder), []) }; } // Any URI @@ -182,11 +171,26 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain return createHash('md5').update(workspaceConfigPath).digest('hex'); } + getWorkspaceIdentifier(workspacePath: URI): IWorkspaceIdentifier { + if (workspacePath.scheme === Schemas.file) { + const configPath = fsPath(workspacePath); + return { + configPath, + id: this.getWorkspaceId(configPath) + }; + } + throw new Error('Not yet supported'); + /*return { + configPath: workspacePath + id: this.getWorkspaceId(workspacePath.toString()); + };*/ + } + isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean { return this.isInsideWorkspacesHome(workspace.configPath); } - saveWorkspace(workspace: IWorkspaceIdentifier, targetConfigPath: string): Promise { + saveWorkspaceAs(workspace: IWorkspaceIdentifier, targetConfigPath: string): Promise { // Return early if target is same as source if (isEqual(workspace.configPath, targetConfigPath, !isLinux)) { @@ -195,48 +199,11 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain // Read the contents of the workspace file and resolve it return readFile(workspace.configPath).then(raw => { - const rawWorkspaceContents = raw.toString(); - let storedWorkspace: IStoredWorkspace; - try { - storedWorkspace = this.doParseStoredWorkspace(workspace.configPath, rawWorkspaceContents); - } catch (error) { - return Promise.reject(error); - } - - const sourceConfigFolder = dirname(workspace.configPath); - const targetConfigFolder = dirname(targetConfigPath); - - // Rewrite absolute paths to relative paths if the target workspace folder - // is a parent of the location of the workspace file itself. Otherwise keep - // using absolute paths. - storedWorkspace.folders.forEach(folder => { - if (isRawFileWorkspaceFolder(folder)) { - if (!isAbsolute(folder.path)) { - folder.path = resolve(sourceConfigFolder, folder.path); // relative paths get resolved against the workspace location - } - folder.path = massageFolderPathForWorkspace(folder.path, targetConfigFolder, storedWorkspace.folders); - } - - }); - - // Preserve as much of the existing workspace as possible by using jsonEdit - // and only changing the folders portion. - let newRawWorkspaceContents = rawWorkspaceContents; - const edits = jsonEdit.setProperty(rawWorkspaceContents, ['folders'], storedWorkspace.folders, { insertSpaces: false, tabSize: 4, eol: (isLinux || isMacintosh) ? '\n' : '\r\n' }); - edits.forEach(edit => { - newRawWorkspaceContents = jsonEdit.applyEdit(rawWorkspaceContents, edit); - }); + const targetConfigPathURI = URI.file(targetConfigPath); + const newRawWorkspaceContents = rewriteWorkspaceFileForNewLocation(raw.toString(), URI.file(workspace.configPath), targetConfigPathURI); return writeFile(targetConfigPath, newRawWorkspaceContents).then(() => { - const savedWorkspaceIdentifier = { id: this.getWorkspaceId(targetConfigPath), configPath: targetConfigPath }; - - // Event - this._onWorkspaceSaved.fire({ workspace: savedWorkspaceIdentifier, oldConfigPath: workspace.configPath }); - - // Delete untitled workspace - this.deleteUntitledWorkspaceSync(workspace); - - return savedWorkspaceIdentifier; + return this.getWorkspaceIdentifier(targetConfigPathURI); }); }); } diff --git a/src/vs/platform/workspaces/node/workspaces.ts b/src/vs/platform/workspaces/node/workspaces.ts index 723d8ea2499..e5e872b9f19 100644 --- a/src/vs/platform/workspaces/node/workspaces.ts +++ b/src/vs/platform/workspaces/node/workspaces.ts @@ -3,11 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IStoredWorkspaceFolder, isRawFileWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; -import { isWindows, isLinux } from 'vs/base/common/platform'; -import { isAbsolute, relative } from 'path'; -import { isEqualOrParent, normalize } from 'vs/base/common/paths'; +import { IStoredWorkspaceFolder, isRawFileWorkspaceFolder, IStoredWorkspace, isStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; +import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; +import { isAbsolute, relative, posix, resolve } from 'path'; +import { normalize, isEqualOrParent } from 'vs/base/common/paths'; import { normalizeDriveLetter } from 'vs/base/common/labels'; +import { URI } from 'vs/base/common/uri'; +import { fsPath, dirname } from 'vs/base/common/resources'; +import { Schemas } from 'vs/base/common/network'; +import * as jsonEdit from 'vs/base/common/jsonEdit'; +import * as json from 'vs/base/common/json'; const SLASH = '/'; @@ -19,33 +24,91 @@ const SLASH = '/'; * @param targetConfigFolder the folder where the workspace is living in * @param existingFolders a set of existing folders of the workspace */ -export function massageFolderPathForWorkspace(absoluteFolderPath: string, targetConfigFolder: string, existingFolders: IStoredWorkspaceFolder[]): string { - const useSlashesForPath = shouldUseSlashForPath(existingFolders); +export function massageFolderPathForWorkspace(absoluteFolderPath: string, targetConfigFolderURI: URI, existingFolders: IStoredWorkspaceFolder[]): string { - // Convert path to relative path if the target config folder - // is a parent of the path. - if (isEqualOrParent(absoluteFolderPath, targetConfigFolder, !isLinux)) { - absoluteFolderPath = relative(targetConfigFolder, absoluteFolderPath) || '.'; - } + if (targetConfigFolderURI.scheme === Schemas.file) { + const targetFolderPath = fsPath(targetConfigFolderURI); + // Convert path to relative path if the target config folder + // is a parent of the path. + if (isEqualOrParent(absoluteFolderPath, targetFolderPath, !isLinux)) { + absoluteFolderPath = relative(targetFolderPath, absoluteFolderPath) || '.'; + } - // Windows gets special treatment: - // - normalize all paths to get nice casing of drive letters - // - convert to slashes if we want to use slashes for paths - if (isWindows) { - if (isAbsolute(absoluteFolderPath)) { - if (useSlashesForPath) { - absoluteFolderPath = normalize(absoluteFolderPath, false /* do not use OS path separator */); + // Windows gets special treatment: + // - normalize all paths to get nice casing of drive letters + // - convert to slashes if we want to use slashes for paths + if (isWindows) { + if (isAbsolute(absoluteFolderPath)) { + if (shouldUseSlashForPath(existingFolders)) { + absoluteFolderPath = normalize(absoluteFolderPath, false /* do not use OS path separator */); + } + + absoluteFolderPath = normalizeDriveLetter(absoluteFolderPath); + } else if (shouldUseSlashForPath(existingFolders)) { + absoluteFolderPath = absoluteFolderPath.replace(/[\\]/g, SLASH); } - - absoluteFolderPath = normalizeDriveLetter(absoluteFolderPath); - } else if (useSlashesForPath) { - absoluteFolderPath = absoluteFolderPath.replace(/[\\]/g, SLASH); + } + } else { + if (isEqualOrParent(absoluteFolderPath, targetConfigFolderURI.path)) { + absoluteFolderPath = posix.relative(absoluteFolderPath, targetConfigFolderURI.path) || '.'; } } return absoluteFolderPath; } +/** + * 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, targetConfigPathURI: URI) { + let storedWorkspace = doParseStoredWorkspace(configPathURI, rawWorkspaceContents); + + const sourceConfigFolder = dirname(configPathURI)!; + const targetConfigFolder = dirname(targetConfigPathURI)!; + + // Rewrite absolute paths to relative paths if the target workspace folder + // is a parent of the location of the workspace file itself. Otherwise keep + // using absolute paths. + for (const folder of storedWorkspace.folders) { + if (isRawFileWorkspaceFolder(folder)) { + if (sourceConfigFolder.scheme === Schemas.file) { + if (!isAbsolute(folder.path)) { + folder.path = resolve(fsPath(sourceConfigFolder), folder.path); // relative paths get resolved against the workspace location + } + folder.path = massageFolderPathForWorkspace(folder.path, targetConfigFolder, storedWorkspace.folders); + } + } + } + + // Preserve as much of the existing workspace as possible by using jsonEdit + // and only changing the folders portion. + let newRawWorkspaceContents = rawWorkspaceContents; + const edits = jsonEdit.setProperty(rawWorkspaceContents, ['folders'], storedWorkspace.folders, { insertSpaces: false, tabSize: 4, eol: (isLinux || isMacintosh) ? '\n' : '\r\n' }); + edits.forEach(edit => { + newRawWorkspaceContents = jsonEdit.applyEdit(rawWorkspaceContents, edit); + }); + return newRawWorkspaceContents; +} + +function 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 (Array.isArray(storedWorkspace.folders)) { + storedWorkspace.folders = storedWorkspace.folders.filter(folder => isStoredWorkspaceFolder(folder)); + } + + // Validate + if (!Array.isArray(storedWorkspace.folders)) { + throw new Error(`${path} looks like an invalid workspace file.`); + } + + return storedWorkspace; +} + function shouldUseSlashForPath(storedFolders: IStoredWorkspaceFolder[]): boolean { // Determine which path separator to use: diff --git a/src/vs/platform/workspaces/node/workspacesIpc.ts b/src/vs/platform/workspaces/node/workspacesIpc.ts index bfd39ea3e0c..6b44c1dedea 100644 --- a/src/vs/platform/workspaces/node/workspacesIpc.ts +++ b/src/vs/platform/workspaces/node/workspacesIpc.ts @@ -18,7 +18,7 @@ export class WorkspacesChannel implements IServerChannel { call(_, command: string, arg?: any): Promise { switch (command) { - case 'createWorkspace': { + case 'createUntitledWorkspace': { const rawFolders: IWorkspaceFolderCreationData[] = arg; let folders: IWorkspaceFolderCreationData[] | undefined = undefined; if (Array.isArray(rawFolders)) { @@ -30,7 +30,7 @@ export class WorkspacesChannel implements IServerChannel { }); } - return this.service.createWorkspace(folders); + return this.service.createUntitledWorkspace(folders); } } @@ -44,7 +44,7 @@ export class WorkspacesChannelClient implements IWorkspacesService { constructor(private channel: IChannel) { } - createWorkspace(folders?: IWorkspaceFolderCreationData[]): Promise { - return this.channel.call('createWorkspace', folders); + createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[]): Promise { + return this.channel.call('createUntitledWorkspace', folders); } } diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts index bd72f12615e..914930bb9ff 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts @@ -12,7 +12,7 @@ import * as pfs from 'vs/base/node/pfs'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { parseArgs } from 'vs/platform/environment/node/argv'; import { WorkspacesMainService, IStoredWorkspace } from 'vs/platform/workspaces/electron-main/workspacesMainService'; -import { WORKSPACE_EXTENSION, IWorkspaceSavedEvent, IWorkspaceIdentifier, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; +import { WORKSPACE_EXTENSION, IWorkspaceIdentifier, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder } 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/workbench/test/workbenchTestServices'; @@ -40,11 +40,11 @@ suite('WorkspacesMainService', () => { } function createWorkspace(folders: string[], names?: string[]) { - return service.createWorkspace(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData))); + return service.createUntitledWorkspace(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData))); } function createWorkspaceSync(folders: string[], names?: string[]) { - return service.createWorkspaceSync(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData))); + 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), process.execPath); @@ -106,8 +106,8 @@ suite('WorkspacesMainService', () => { }); }); - test('createWorkspace (folders as other resource URIs)', () => { - return service.createWorkspace([{ uri: URI.from({ scheme: 'myScheme', path: process.cwd() }) }, { uri: URI.from({ scheme: 'myScheme', path: os.tmpdir() }) }]).then(workspace => { + test('createUntitledWorkspace (folders as other resource URIs)', () => { + return service.createUntitledWorkspace([{ uri: URI.from({ scheme: 'myScheme', path: process.cwd() }) }, { uri: URI.from({ scheme: 'myScheme', path: os.tmpdir() }) }]).then(workspace => { assert.ok(workspace); assert.ok(fs.existsSync(workspace.configPath)); assert.ok(service.isUntitledWorkspace(workspace)); @@ -152,8 +152,8 @@ suite('WorkspacesMainService', () => { assert.equal((ws.folders[1]).name, 'tempdir'); }); - test('createWorkspaceSync (folders as other resource URIs)', () => { - const workspace = service.createWorkspaceSync([{ uri: URI.from({ scheme: 'myScheme', path: process.cwd() }) }, { uri: URI.from({ scheme: 'myScheme', path: os.tmpdir() }) }]); + test('createUntitledWorkspaceSync (folders as other resource URIs)', () => { + const workspace = service.createUntitledWorkspaceSync([{ uri: URI.from({ scheme: 'myScheme', path: process.cwd() }) }, { uri: URI.from({ scheme: 'myScheme', path: os.tmpdir() }) }]); assert.ok(workspace); assert.ok(fs.existsSync(workspace.configPath)); assert.ok(service.isUntitledWorkspace(workspace)); @@ -224,40 +224,20 @@ suite('WorkspacesMainService', () => { }); test('saveWorkspace (untitled)', () => { - let savedEvent: IWorkspaceSavedEvent; - const listener = service.onWorkspaceSaved(e => { - savedEvent = e; - }); - - let deletedEvent: IWorkspaceIdentifier; - const listener2 = service.onUntitledWorkspaceDeleted(e => { - deletedEvent = e; - }); - return createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]).then(workspace => { const workspaceConfigPath = path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`); - return service.saveWorkspace(workspace, workspaceConfigPath).then(savedWorkspace => { + return service.saveWorkspaceAs(workspace, workspaceConfigPath).then(savedWorkspace => { assert.ok(savedWorkspace.id); assert.notEqual(savedWorkspace.id, workspace.id); assert.equal(savedWorkspace.configPath, workspaceConfigPath); - assert.equal(service.deleteWorkspaceCall, workspace); - const ws = JSON.parse(fs.readFileSync(savedWorkspace.configPath).toString()) as IStoredWorkspace; assert.equal(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, process.cwd()); // absolute assertPathEquals((ws.folders[1]).path, '.'); // relative assertPathEquals((ws.folders[2]).path, path.relative(path.dirname(workspaceConfigPath), path.join(os.tmpdir(), 'somefolder'))); // relative - assert.equal(savedWorkspace, savedEvent.workspace); - assertPathEquals(workspace.configPath, savedEvent.oldConfigPath); - - assert.deepEqual(deletedEvent, workspace); - - listener.dispose(); - listener2.dispose(); - extfs.delSync(workspaceConfigPath); }); }); @@ -268,8 +248,8 @@ suite('WorkspacesMainService', () => { const workspaceConfigPath = path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`); const newWorkspaceConfigPath = path.join(os.tmpdir(), `mySavedWorkspace.${Date.now()}.${WORKSPACE_EXTENSION}`); - return service.saveWorkspace(workspace, workspaceConfigPath).then(savedWorkspace => { - return service.saveWorkspace(savedWorkspace, newWorkspaceConfigPath).then(newSavedWorkspace => { + return service.saveWorkspaceAs(workspace, workspaceConfigPath).then(savedWorkspace => { + return service.saveWorkspaceAs(savedWorkspace, newWorkspaceConfigPath).then(newSavedWorkspace => { assert.ok(newSavedWorkspace.id); assert.notEqual(newSavedWorkspace.id, workspace.id); assertPathEquals(newSavedWorkspace.configPath, newWorkspaceConfigPath); @@ -292,11 +272,11 @@ suite('WorkspacesMainService', () => { const workspaceConfigPath = path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`); const newWorkspaceConfigPath = path.join(os.tmpdir(), `mySavedWorkspace.${Date.now()}.${WORKSPACE_EXTENSION}`); - return service.saveWorkspace(workspace, workspaceConfigPath).then(savedWorkspace => { + return service.saveWorkspaceAs(workspace, workspaceConfigPath).then(savedWorkspace => { const contents = fs.readFileSync(savedWorkspace.configPath).toString(); fs.writeFileSync(savedWorkspace.configPath, `// this is a comment\n${contents}`); - return service.saveWorkspace(savedWorkspace, newWorkspaceConfigPath).then(newSavedWorkspace => { + return service.saveWorkspaceAs(savedWorkspace, newWorkspaceConfigPath).then(newSavedWorkspace => { assert.ok(newSavedWorkspace.id); assert.notEqual(newSavedWorkspace.id, workspace.id); assertPathEquals(newSavedWorkspace.configPath, newWorkspaceConfigPath); @@ -316,11 +296,11 @@ suite('WorkspacesMainService', () => { const workspaceConfigPath = path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`); const newWorkspaceConfigPath = path.join(os.tmpdir(), `mySavedWorkspace.${Date.now()}.${WORKSPACE_EXTENSION}`); - return service.saveWorkspace(workspace, workspaceConfigPath).then(savedWorkspace => { + return service.saveWorkspaceAs(workspace, workspaceConfigPath).then(savedWorkspace => { const contents = fs.readFileSync(savedWorkspace.configPath).toString(); fs.writeFileSync(savedWorkspace.configPath, contents.replace(/[\\]/g, '/')); // convert backslash to slash - return service.saveWorkspace(savedWorkspace, newWorkspaceConfigPath).then(newSavedWorkspace => { + return service.saveWorkspaceAs(savedWorkspace, newWorkspaceConfigPath).then(newSavedWorkspace => { assert.ok(newSavedWorkspace.id); assert.notEqual(newSavedWorkspace.id, workspace.id); assertPathEquals(newSavedWorkspace.configPath, newWorkspaceConfigPath); @@ -349,7 +329,7 @@ suite('WorkspacesMainService', () => { return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => { const workspaceConfigPath = path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`); - return service.saveWorkspace(workspace, workspaceConfigPath).then(savedWorkspace => { + return service.saveWorkspaceAs(workspace, workspaceConfigPath).then(savedWorkspace => { assert.ok(fs.existsSync(savedWorkspace.configPath)); service.deleteUntitledWorkspaceSync(savedWorkspace); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index fd07204c108..b2d1771d638 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -70,7 +70,7 @@ declare module 'vscode' { /** * The offset of the first character which is not a whitespace character as defined - * by `/\s/`. **Note** that if a line is all whitespaces the length of the line is returned. + * by `/\s/`. **Note** that if a line is all whitespace the length of the line is returned. */ readonly firstNonWhitespaceCharacterIndex: number; @@ -3218,7 +3218,7 @@ declare module 'vscode' { /** * List of characters that re-trigger signature help. * - * These trigger characters are only active when signature help is alread showing. All trigger characters + * These trigger characters are only active when signature help is already showing. All trigger characters * are also counted as re-trigger characters. */ readonly retriggerCharacters: ReadonlyArray; @@ -4681,7 +4681,7 @@ declare module 'vscode' { storagePath: string | undefined; /** - * An absolute file path in which the extension can store gloabal state. + * An absolute file path in which the extension can store global state. * The directory might not exist on disk and creation is * up to the extension. However, the parent directory is guaranteed to be existent. * @@ -5633,7 +5633,7 @@ declare module 'vscode' { * * @param source The existing file. * @param destination The destination location. - * @param options Defines if existing files should be overwriten. + * @param options Defines if existing files should be overwritten. * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `source` doesn't exist. * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when parent of `destination` doesn't exist, e.g. no mkdirp-logic required. * @throws [`FileExists`](#FileSystemError.FileExists) when `destination` exists and when the `overwrite` option is not `true`. @@ -5855,7 +5855,7 @@ declare module 'vscode' { */ interface WebviewPanelSerializer { /** - * Restore a webview panel from its seriailzed `state`. + * Restore a webview panel from its serialized `state`. * * Called when a serialized webview first becomes visible. * @@ -5863,7 +5863,7 @@ declare module 'vscode' { * serializer must restore the webview's `.html` and hook up all webview events. * @param state Persisted state from the webview content. * - * @return Thanble indicating that the webview has been fully restored. + * @return Thenble indicating that the webview has been fully restored. */ deserializeWebviewPanel(webviewPanel: WebviewPanel, state: any): Thenable; } @@ -5921,6 +5921,18 @@ declare module 'vscode' { * Changes each time the editor is started. */ export const sessionId: string; + + /** + * Opens an *external* item, e.g. a http(s) or mailto-link, using the + * default application. + * + * *Note* that [`showTextDocument`](#window.showTextDocument) is the right + * way to open a text document inside the editor, not this function. + * + * @param target The uri that should be opened. + * @returns A promise indicating if open was successful. + */ + export function open(target: Uri): Thenable; } /** @@ -6914,13 +6926,13 @@ declare module 'vscode' { } /** - * A light-weight user input UI that is intially not visible. After + * A light-weight user input UI that is initially not visible. After * configuring it through its properties the extension can make it * visible by calling [QuickInput.show](#QuickInput.show). * * There are several reasons why this UI might have to be hidden and * the extension will be notified through [QuickInput.onDidHide](#QuickInput.onDidHide). - * (Examples include: an explict call to [QuickInput.hide](#QuickInput.hide), + * (Examples include: an explicit call to [QuickInput.hide](#QuickInput.hide), * the user pressing Esc, some other input UI opening, etc.) * * A user pressing Enter or some other gesture implying acceptance @@ -6989,7 +7001,7 @@ declare module 'vscode' { * * There are several reasons why this UI might have to be hidden and * the extension will be notified through [QuickInput.onDidHide](#QuickInput.onDidHide). - * (Examples include: an explict call to [QuickInput.hide](#QuickInput.hide), + * (Examples include: an explicit call to [QuickInput.hide](#QuickInput.hide), * the user pressing Esc, some other input UI opening, etc.) */ onDidHide: Event; @@ -7483,7 +7495,7 @@ declare module 'vscode' { * cause failure of the operation. * * When applying a workspace edit that consists only of text edits an 'all-or-nothing'-strategy is used. - * A workspace edit with resource creations or deletions aborts the operation, e.g. consective edits will + * A workspace edit with resource creations or deletions aborts the operation, e.g. consecutive edits will * not be attempted, when a single edit fails. * * @param edit A workspace edit. @@ -8522,7 +8534,7 @@ declare module 'vscode' { */ onWillStopSession?(): void; /** - * An error with the debug adapter has occured. + * An error with the debug adapter has occurred. */ onError?(error: Error): void; /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 38f45c1af80..c4e1f669ec7 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -16,25 +16,6 @@ declare module 'vscode' { - //#region Joh - vscode.open - - export namespace env { - - /** - * Opens an *external* item, e.g. a http(s) or mailto-link, using the - * default application. - * - * *Note* that [`showTextDocument`](#window.showTextDocument) is the right - * way to open a text document inside the editor, not this function. - * - * @param target The uri that should be opened. - * @returns A promise indicating if open was successful. - */ - export function open(target: Uri): Thenable; - } - - //#endregion - //#region Joh - selection range provider export class SelectionRangeKind { @@ -45,7 +26,7 @@ declare module 'vscode' { static readonly Empty: SelectionRangeKind; /** - * The statment kind, its value is `statement`, possible extensions can be + * The statement kind, its value is `statement`, possible extensions can be * `statement.if` etc */ static readonly Statement: SelectionRangeKind; @@ -86,7 +67,7 @@ declare module 'vscode' { //#region Joh - read/write in chunks export interface FileSystemProvider { - open?(resource: Uri): number | Thenable; + open?(resource: Uri, options: { create: boolean }): number | Thenable; close?(fd: number): void | Thenable; read?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): number | Thenable; write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): number | Thenable; @@ -812,6 +793,7 @@ declare module 'vscode' { command?: Command; isDraft?: boolean; + commentReactions?: CommentReaction[]; } export interface CommentThreadChangedEvent { @@ -836,6 +818,11 @@ declare module 'vscode' { readonly inDraftMode: boolean; } + interface CommentReaction { + readonly label?: string; + readonly hasReacted?: boolean; + } + interface DocumentCommentProvider { /** * Provide the commenting ranges and comment threads for the given document. The comments are displayed within the editor. @@ -870,6 +857,10 @@ declare module 'vscode' { deleteDraftLabel?: string; finishDraftLabel?: string; + addReaction?(document: TextDocument, comment: Comment, reaction: CommentReaction): Promise; + deleteReaction?(document: TextDocument, comment: Comment, reaction: CommentReaction): Promise; + reactionGroup?: CommentReaction[]; + /** * Notify of updates to comment threads. */ @@ -1004,7 +995,7 @@ declare module 'vscode' { * ```typescript * const terminalRenderer = window.createTerminalRenderer('test'); * terminalRenderer.onDidAcceptInput(data => { - * cosole.log(data); // 'Hello world' + * console.log(data); // 'Hello world' * }); * terminalRenderer.terminal.then(t => t.sendText('Hello world')); * ``` @@ -1064,6 +1055,15 @@ declare module 'vscode' { } //#endregion + //#region Alex - extensions.all change event + export namespace extensions { + /** + * An event which fires when `extensions.all` changes. + */ + export const onDidChange: Event; + } + //#endregion + //#region Tree View export interface TreeView { @@ -1112,7 +1112,8 @@ declare module 'vscode' { /** * The currently active [`SignatureHelp`](#SignatureHelp). * - * Will have the [`SignatureHelp.activeSignature`] field updated based on user arrowing through sig help + * The `activeSignatureHelp` has its [`SignatureHelp.activeSignature`] field updated based on + * the user arrowing through available signatures. */ readonly activeSignatureHelp?: SignatureHelp; } @@ -1197,7 +1198,7 @@ declare module 'vscode' { //#region CodeAction.isPreferred - mjbvz export interface CodeAction { /** - * If the action is a preferred action or fix to take. + * Marks this as a preferred action. Preferred actions are used by the `auto fix` command. * * A quick fix should be marked preferred if it properly addresses the underlying error. * A refactoring should be marked preferred if it is the most reasonable choice of actions to take. @@ -1206,13 +1207,21 @@ declare module 'vscode' { } //#endregion + //#region Tasks + export interface TaskPresentationOptions { + /** + * Controls whether the task is executed in a specific terminal group using split panes. + */ + group?: string; + } + //#endregion //#region Autofix - mjbvz export namespace CodeActionKind { /** - * Base kind for an auto fix source action: `source.fixAll`. + * Base kind for auto-fix source actions: `source.fixAll`. * - * Fix all actions automatically fix errors in the code that have a clear fix that does not require user input. + * Fix all actions automatically fix errors that have a clear fix that do not require user input. * They should not suppress errors or perform unsafe fixes such as generating new types or classes. */ export const SourceFixAll: CodeActionKind; diff --git a/src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts similarity index 99% rename from src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts rename to src/vs/workbench/api/common/configurationExtensionPoint.ts index 151bc66d10a..596500d9c4c 100644 --- a/src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -84,7 +84,6 @@ const defaultConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint { return this._proxy.$finishDraft(this._handle, uri); } + async addReaction(uri, comment: modes.Comment, reaction: modes.CommentReaction, token): Promise { + return this._proxy.$addReaction(this._handle, uri, comment, reaction); + } + async deleteReaction(uri, comment: modes.Comment, reaction: modes.CommentReaction, token): Promise { + return this._proxy.$deleteReaction(this._handle, uri, comment, reaction); + } + onDidChangeCommentThreads = null; } diff --git a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts index 81ca13c1694..2eac8972c17 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts @@ -14,7 +14,7 @@ import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostC import severity from 'vs/base/common/severity'; import { AbstractDebugAdapter } from 'vs/workbench/parts/debug/node/debugAdapter'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { convertToVSCPaths, convertToDAPaths, stringToUri, uriToString } from 'vs/workbench/parts/debug/common/debugUtils'; +import { convertToVSCPaths, convertToDAPaths } from 'vs/workbench/parts/debug/common/debugUtils'; @extHostNamedCustomer(MainContext.MainThreadDebugService) export class MainThreadDebugService implements MainThreadDebugServiceShape, IDebugAdapterFactory { @@ -259,8 +259,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } public $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage) { - - this._debugAdapters.get(handle).acceptMessage(convertToVSCPaths(message, source => uriToString(source))); + this._debugAdapters.get(handle).acceptMessage(convertToVSCPaths(message, false)); } public $acceptDAError(handle: number, name: string, message: string, stack: string) { @@ -345,8 +344,7 @@ class ExtensionHostDebugAdapter extends AbstractDebugAdapter { } public sendMessage(message: DebugProtocol.ProtocolMessage): void { - - this._proxy.$sendDAMessage(this._handle, convertToDAPaths(message, source => stringToUri(source))); + this._proxy.$sendDAMessage(this._handle, convertToDAPaths(message, true)); } public stopSession(): Promise { diff --git a/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts index 0a948e1cfbb..caea6c69d7f 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { FileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IFileSystemProvider, IStat, IWatchOptions, FileType, FileOverwriteOptions, FileDeleteOptions } from 'vs/platform/files/common/files'; +import { FileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IFileSystemProvider, IStat, IWatchOptions, FileType, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions } from 'vs/platform/files/common/files'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { ExtHostContext, ExtHostFileSystemShape, IExtHostContext, IFileChangeDto, MainContext, MainThreadFileSystemShape } from '../node/extHost.protocol'; import { ResourceLabelFormatter, ILabelService } from 'vs/platform/label/common/label'; @@ -130,8 +130,8 @@ class RemoteFileSystemProvider implements IFileSystemProvider { return this._proxy.$copy(this._handle, resource, target, opts); } - open(resource: URI): Promise { - return this._proxy.$open(this._handle, resource); + open(resource: URI, opts: FileOpenOptions): Promise { + return this._proxy.$open(this._handle, resource, opts); } close(fd: number): Promise { diff --git a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts index 167e484b36f..8bb51ccea58 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts @@ -12,7 +12,6 @@ import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostC export class MainThreadTerminalService implements MainThreadTerminalServiceShape { private _proxy: ExtHostTerminalServiceShape; - private _remoteAuthority: string | null; private _toDispose: IDisposable[] = []; private _terminalProcesses: { [id: number]: ITerminalProcessExtHostProxy } = {}; private _terminalOnDidWriteDataListeners: { [id: number]: IDisposable } = {}; @@ -23,7 +22,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape @ITerminalService private readonly terminalService: ITerminalService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); - this._remoteAuthority = extHostContext.remoteAuthority; this._toDispose.push(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 @@ -204,11 +202,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } private _onTerminalRequestExtHostProcess(request: ITerminalProcessExtHostRequest): void { - // Only allow processes on remote ext hosts - if (!this._remoteAuthority) { - return; - } - this._terminalProcesses[request.proxy.terminalId] = request.proxy; const shellLaunchConfigDto: ShellLaunchConfigDto = { name: request.shellLaunchConfig.name, @@ -221,6 +214,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape request.proxy.onInput(data => this._proxy.$acceptProcessInput(request.proxy.terminalId, data)); request.proxy.onResize(dimensions => this._proxy.$acceptProcessResize(request.proxy.terminalId, dimensions.cols, dimensions.rows)); request.proxy.onShutdown(immediate => this._proxy.$acceptProcessShutdown(request.proxy.terminalId, immediate)); + request.proxy.onRequestCwd(() => this._proxy.$acceptProcessRequestCwd(request.proxy.terminalId)); + request.proxy.onRequestInitialCwd(() => this._proxy.$acceptProcessRequestInitialCwd(request.proxy.terminalId)); } public $sendProcessTitle(terminalId: number, title: string): void { @@ -239,4 +234,12 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._terminalProcesses[terminalId].emitExit(exitCode); delete this._terminalProcesses[terminalId]; } + + public $sendProcessInitialCwd(terminalId: number, initialCwd: string): void { + this._terminalProcesses[terminalId].emitInitialCwd(initialCwd); + } + + public $sendProcessCwd(terminalId: number, cwd: string): void { + this._terminalProcesses[terminalId].emitCwd(cwd); + } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index da0ef360fdd..6a435ed4660 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -131,11 +131,6 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { return undefined; // invalid query parameters } - const useRipgrep = folderQueries.every(folderQuery => { - const folderConfig = this._configurationService.getValue({ resource: folderQuery.folder }); - return folderConfig.search.useRipgrep; - }); - const ignoreSymlinks = folderQueries.every(folderQuery => { const folderConfig = this._configurationService.getValue({ resource: folderQuery.folder }); return !folderConfig.search.followSymlinks; @@ -151,7 +146,6 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { type: QueryType.File, maxResults, disregardExcludeSettings: excludePatternOrDisregardExcludes === false, - useRipgrep, _reason: 'startFileSearch' }; if (typeof includePattern === 'string') { @@ -251,5 +245,5 @@ CommandsRegistry.registerCommand('_workbench.enterWorkspace', async function (ac } } - return workspaceEditingService.enterWorkspace(workspace.fsPath); + return workspaceEditingService.enterWorkspace(workspace); }); diff --git a/src/vs/workbench/api/node/apiCommands.ts b/src/vs/workbench/api/node/apiCommands.ts index 0e228988d60..0e808dbfed4 100644 --- a/src/vs/workbench/api/node/apiCommands.ts +++ b/src/vs/workbench/api/node/apiCommands.ts @@ -3,14 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { tmpdir } from 'os'; +import { posix } from 'path'; +import * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { isMalformedFileUri } from 'vs/base/common/resources'; -import * as vscode from 'vscode'; import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; import { CommandsRegistry, ICommandService, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { EditorViewColumn } from 'vs/workbench/api/shared/editor'; import { EditorGroupLayout } from 'vs/workbench/services/group/common/editorGroupsService'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWindowsService } from 'vs/platform/windows/common/windows'; +import { IDownloadService } from 'vs/platform/download/common/download'; +import { generateUuid } from 'vs/base/common/uuid'; // ----------------------------------------------------------------- // The following commands are registered on both sides separately. @@ -99,6 +106,12 @@ export class OpenAPICommand { } CommandsRegistry.registerCommand(OpenAPICommand.ID, adjustHandler(OpenAPICommand.execute)); +CommandsRegistry.registerCommand('_workbench.removeFromRecentlyOpened', function (accessor: ServicesAccessor, path: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string) { + const windowsService = accessor.get(IWindowsService); + + return windowsService.removeFromRecentlyOpened([path]).then(() => undefined); +}); + export class RemoveFromRecentlyOpenedAPICommand { public static ID = 'vscode.removeFromRecentlyOpened'; public static execute(executor: ICommandsExecutor, path: string): Promise { @@ -113,4 +126,11 @@ export class SetEditorLayoutAPICommand { return executor.executeCommand('layoutEditorGroups', layout); } } -CommandsRegistry.registerCommand(SetEditorLayoutAPICommand.ID, adjustHandler(SetEditorLayoutAPICommand.execute)); \ No newline at end of file +CommandsRegistry.registerCommand(SetEditorLayoutAPICommand.ID, adjustHandler(SetEditorLayoutAPICommand.execute)); + +CommandsRegistry.registerCommand('_workbench.downloadResource', function (accessor: ServicesAccessor, resource: URI) { + const downloadService = accessor.get(IDownloadService); + const location = posix.join(tmpdir(), generateUuid()); + + return downloadService.download(resource, location).then(() => URI.file(location)); +}); \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 7f58bbf7625..df6de67f67e 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -256,9 +256,9 @@ export function createApiFactory( get clipboard(): vscode.Clipboard { return extHostClipboard; }, - open: proposedApiFunction(extension, (uri: URI) => { + open(uri: URI) { return extHostWindow.openUri(uri); - }) + } }); // namespace: extensions @@ -272,6 +272,9 @@ export function createApiFactory( }, get all(): Extension[] { return extensionRegistry.getAllExtensionDescriptions().map((desc) => new Extension(extensionService, desc)); + }, + get onDidChange() { + return extensionRegistry.onDidChange; } }; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index fcc259dded9..3c55d1f6c67 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -20,7 +20,7 @@ import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/modes/ import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { ConfigurationTarget, IConfigurationData, IConfigurationModel } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { FileChangeType, FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileType, FileWriteOptions, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { FileChangeType, FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileType, FileWriteOptions, IStat, IWatchOptions, FileOpenOptions } from 'vs/platform/files/common/files'; import { ResourceLabelFormatter } from 'vs/platform/label/common/label'; import { LogLevel } from 'vs/platform/log/common/log'; import { IMarkerData } from 'vs/platform/markers/common/markers'; @@ -110,6 +110,7 @@ export interface CommentProviderFeatures { startDraftLabel?: string; deleteDraftLabel?: string; finishDraftLabel?: string; + reactionGroup?: vscode.CommentReaction[]; } export interface MainThreadCommentsShape extends IDisposable { @@ -362,6 +363,8 @@ export interface MainThreadTerminalServiceShape extends IDisposable { $sendProcessData(terminalId: number, data: string): void; $sendProcessPid(terminalId: number, pid: number): void; $sendProcessExit(terminalId: number, exitCode: number): void; + $sendProcessInitialCwd(terminalId: number, cwd: string): void; + $sendProcessCwd(terminalId: number, initialCwd: string): void; // Renderer $terminalRendererSetName(terminalId: number, name: string): void; @@ -732,7 +735,7 @@ export interface ExtHostFileSystemShape { $delete(handle: number, resource: UriComponents, opts: FileDeleteOptions): Promise; $watch(handle: number, session: number, resource: UriComponents, opts: IWatchOptions): void; $unwatch(handle: number, session: number): void; - $open(handle: number, resource: UriComponents): Promise; + $open(handle: number, resource: UriComponents, opts: FileOpenOptions): Promise; $close(handle: number, fd: number): Promise; $read(handle: number, fd: number, pos: number, length: number): Promise; $write(handle: number, fd: number, pos: number, data: Buffer): Promise; @@ -939,6 +942,8 @@ export interface ExtHostTerminalServiceShape { $acceptProcessInput(id: number, data: string): void; $acceptProcessResize(id: number, cols: number, rows: number): void; $acceptProcessShutdown(id: number, immediate: boolean): void; + $acceptProcessRequestInitialCwd(id: number): void; + $acceptProcessRequestCwd(id: number): void; } export interface ExtHostSCMShape { @@ -1065,6 +1070,8 @@ export interface ExtHostCommentsShape { $startDraft(handle: number, document: UriComponents): Promise; $deleteDraft(handle: number, document: UriComponents): Promise; $finishDraft(handle: number, document: UriComponents): Promise; + $addReaction(handle: number, document: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise; + $deleteReaction(handle: number, document: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise; $provideWorkspaceComments(handle: number): Promise; } diff --git a/src/vs/workbench/api/node/extHostCommands.ts b/src/vs/workbench/api/node/extHostCommands.ts index 8a6b94a362d..4e91ca97d00 100644 --- a/src/vs/workbench/api/node/extHostCommands.ts +++ b/src/vs/workbench/api/node/extHostCommands.ts @@ -15,6 +15,9 @@ import * as modes from 'vs/editor/common/modes'; import * as vscode from 'vscode'; import { ILogService } from 'vs/platform/log/common/log'; import { revive } from 'vs/base/common/marshalling'; +import { Range } from 'vs/editor/common/core/range'; +import { Position } from 'vs/editor/common/core/position'; +import { URI } from 'vs/base/common/uri'; interface CommandHandler { callback: Function; @@ -42,7 +45,33 @@ export class ExtHostCommands implements ExtHostCommandsShape { this._proxy = mainContext.getProxy(MainContext.MainThreadCommands); this._logService = logService; this._converter = new CommandsConverter(this, heapService); - this._argumentProcessors = [{ processArgument(a) { return revive(a, 0); } }]; + this._argumentProcessors = [ + { + processArgument(a) { + // URI, Regex + return revive(a, 0); + } + }, + { + processArgument(arg) { + return cloneAndChange(arg, function (obj) { + // Reverse of https://github.com/Microsoft/vscode/blob/1f28c5fc681f4c01226460b6d1c7e91b8acb4a5b/src/vs/workbench/api/node/extHostCommands.ts#L112-L127 + if (Range.isIRange(obj)) { + return extHostTypeConverter.Range.to(obj); + } + if (Position.isIPosition(obj)) { + return extHostTypeConverter.Position.to(obj); + } + if (Range.isIRange((obj as modes.Location).range) && URI.isUri((obj as modes.Location).uri)) { + return extHostTypeConverter.location.to(obj); + } + if (!Array.isArray(obj)) { + return obj; + } + }); + } + } + ]; } get converter(): CommandsConverter { diff --git a/src/vs/workbench/api/node/extHostComments.ts b/src/vs/workbench/api/node/extHostComments.ts index e814ea68a15..3e76d66a03e 100644 --- a/src/vs/workbench/api/node/extHostComments.ts +++ b/src/vs/workbench/api/node/extHostComments.ts @@ -69,7 +69,8 @@ export class ExtHostComments implements ExtHostCommentsShape { this._proxy.$registerDocumentCommentProvider(handle, { startDraftLabel: provider.startDraftLabel, deleteDraftLabel: provider.deleteDraftLabel, - finishDraftLabel: provider.finishDraftLabel + finishDraftLabel: provider.finishDraftLabel, + reactionGroup: provider.reactionGroup }); this.registerListeners(handle, extensionId, provider); @@ -174,6 +175,34 @@ export class ExtHostComments implements ExtHostCommentsShape { }); } + $addReaction(handle: number, uri: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise { + const data = this._documents.getDocumentData(URI.revive(uri)); + + if (!data || !data.document) { + throw new Error('Unable to retrieve document from URI'); + } + + const handlerData = this._documentProviders.get(handle); + + return asPromise(() => { + return handlerData.provider.addReaction(data.document, convertFromComment(comment), reaction); + }); + } + + $deleteReaction(handle: number, uri: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise { + const data = this._documents.getDocumentData(URI.revive(uri)); + + if (!data || !data.document) { + throw new Error('Unable to retrieve document from URI'); + } + + const handlerData = this._documentProviders.get(handle); + + return asPromise(() => { + return handlerData.provider.deleteReaction(data.document, convertFromComment(comment), reaction); + }); + } + $provideDocumentComments(handle: number, uri: UriComponents): Promise { const data = this._documents.getDocumentData(URI.revive(uri)); if (!data || !data.document) { @@ -259,7 +288,8 @@ function convertFromComment(comment: modes.Comment): vscode.Comment { userIconPath: userIconPath, canEdit: comment.canEdit, canDelete: comment.canDelete, - isDraft: comment.isDraft + isDraft: comment.isDraft, + commentReactions: comment.commentReactions }; } @@ -275,6 +305,7 @@ function convertToComment(provider: vscode.DocumentCommentProvider | vscode.Work canEdit: canEdit, canDelete: canDelete, command: vscodeComment.command ? commandsConverter.toInternal(vscodeComment.command) : null, - isDraft: vscodeComment.isDraft + isDraft: vscodeComment.isDraft, + commentReactions: vscodeComment.commentReactions }; } diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index fcc931e8082..a4b106f656b 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -24,7 +24,7 @@ import { getTerminalLauncher, hasChildProcesses, prepareCommand } from 'vs/workb import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/node/variableResolver'; import { ExtHostConfiguration, ExtHostConfigProvider } from './extHostConfiguration'; -import { convertToVSCPaths, convertToDAPaths, stringToUri, uriToString } from 'vs/workbench/parts/debug/common/debugUtils'; +import { convertToVSCPaths, convertToDAPaths } from 'vs/workbench/parts/debug/common/debugUtils'; import { ExtHostTerminalService } from 'vs/workbench/api/node/extHostTerminalService'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; @@ -422,7 +422,7 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { } // DA -> VS Code - message = convertToVSCPaths(message, source => stringToUri(source)); + message = convertToVSCPaths(message, true); mythis._debugServiceProxy.$acceptDAMessage(debugAdapterHandle, message); }); @@ -454,7 +454,7 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { public $sendDAMessage(debugAdapterHandle: number, message: DebugProtocol.ProtocolMessage): Promise { // VS Code -> DA - message = convertToDAPaths(message, source => uriToString(source)); + message = convertToDAPaths(message, false); const tracker = this._debugAdaptersTrackers.get(debugAdapterHandle); // TODO@AW: same handle? if (tracker) { diff --git a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts index 226248d1699..29c19a7cefc 100644 --- a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { sequence, always } from 'vs/base/common/async'; +import { sequence } from 'vs/base/common/async'; import { illegalState } from 'vs/base/common/errors'; import { ExtHostDocumentSaveParticipantShape, MainThreadTextEditorsShape, ResourceTextEditDto } from 'vs/workbench/api/node/extHost.protocol'; import { TextEdit } from 'vs/workbench/api/node/extHostTypes'; @@ -67,7 +67,7 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic return this._deliverEventAsyncAndBlameBadListeners(listener, { document, reason: TextDocumentSaveReason.to(reason) }); }; })); - return always(promise, () => clearTimeout(didTimeoutHandle)); + return promise.finally(() => clearTimeout(didTimeoutHandle)); } private _deliverEventAsyncAndBlameBadListeners([listener, thisArg, extension]: Listener, stubEvent: vscode.TextDocumentWillSaveEvent): Promise { diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 41dd9ad1fc9..8edbff284ea 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -21,7 +21,7 @@ import { ExtHostStorage } from 'vs/workbench/api/node/extHostStorage'; import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry'; -import { connectProxyResolver } from 'vs/workbench/node/proxyResolver'; +import { connectProxyResolver } from 'vs/workbench/services/extensions/node/proxyResolver'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; import { ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver'; diff --git a/src/vs/workbench/api/node/extHostFileSystem.ts b/src/vs/workbench/api/node/extHostFileSystem.ts index f1fe101d1c1..a1e8edc9204 100644 --- a/src/vs/workbench/api/node/extHostFileSystem.ts +++ b/src/vs/workbench/api/node/extHostFileSystem.ts @@ -280,9 +280,9 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { } } - $open(handle: number, resource: UriComponents): Promise { + $open(handle: number, resource: UriComponents, opts: files.FileOpenOptions): Promise { this._checkProviderExists(handle); - return Promise.resolve(this._fsProvider.get(handle).open(URI.revive(resource))); + return Promise.resolve(this._fsProvider.get(handle).open(URI.revive(resource), opts)); } $close(handle: number, fd: number): Promise { diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 8379fa3ad79..76b8b1fbe00 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -756,11 +756,14 @@ class SignatureHelpAdapter { private reviveContext(context: modes.SignatureHelpContext): vscode.SignatureHelpContext { let activeSignatureHelp: vscode.SignatureHelp | undefined = undefined; if (context.activeSignatureHelp) { + const revivedSignatureHelp = typeConvert.SignatureHelp.to(context.activeSignatureHelp); const saved = this._heap.get(ObjectIdentifier.of(context.activeSignatureHelp)); if (saved) { activeSignatureHelp = saved; + activeSignatureHelp.activeSignature = revivedSignatureHelp.activeSignature; + activeSignatureHelp.activeParameter = revivedSignatureHelp.activeParameter; } else { - activeSignatureHelp = typeConvert.SignatureHelp.to(context.activeSignatureHelp); + activeSignatureHelp = revivedSignatureHelp; } } return { ...context, activeSignatureHelp }; diff --git a/src/vs/workbench/api/node/extHostSearch.fileIndex.ts b/src/vs/workbench/api/node/extHostSearch.fileIndex.ts index 7e32abf71b1..d0ed46e8e2a 100644 --- a/src/vs/workbench/api/node/extHostSearch.fileIndex.ts +++ b/src/vs/workbench/api/node/extHostSearch.fileIndex.ts @@ -576,6 +576,9 @@ export class FileIndexSearchManager { catch(reject?) { return this.then(undefined, reject); } + finally(onFinally) { + return promise.finally(onFinally); + } }; } } diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 496cfcf4ee7..9faeb66533d 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -18,6 +18,7 @@ import { generateRandomPipeName } from 'vs/base/parts/ipc/node/ipc.net'; import * as http from 'http'; import * as fs from 'fs'; import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; +import { sanitizeProcessEnvironment } from 'vs/base/node/processes'; const RENDERER_NO_PROCESS_ID = -1; @@ -452,18 +453,19 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { // TODO: Pull in and resolve config settings // // Resolve env vars from config and shell // const lastActiveWorkspaceRoot = this._workspaceContextService.getWorkspaceFolder(lastActiveWorkspaceRootUri); - // const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); - // const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...this._configHelper.config.env[platformKey] }, lastActiveWorkspaceRoot); + const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); + // const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...terminalConfig.env[platformKey] }, lastActiveWorkspaceRoot); + const envFromConfig = { ...terminalConfig.env[platformKey] }; // const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot); // Merge process env with the env from config const env = { ...process.env }; - // terminalEnvironment.mergeEnvironments(env, envFromConfig); + terminalEnvironment.mergeEnvironments(env, envFromConfig); terminalEnvironment.mergeEnvironments(env, shellLaunchConfig.env); // Sanitize the environment, removing any undesirable VS Code and Electron environment // variables - terminalEnvironment.sanitizeEnvironment(env); + sanitizeProcessEnvironment(env); // Continue env initialization, merging in the env from the launch // config and adding keys that are needed to create the process @@ -476,11 +478,12 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { // Fork the process and listen for messages this._logService.debug(`Terminal process launching on ext host`, shellLaunchConfig, initialCwd, cols, rows, env); - this._terminalProcesses[id] = new TerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, terminalConfig.get('windowsEnableConpty')); - this._terminalProcesses[id].onProcessIdReady(pid => this._proxy.$sendProcessPid(id, pid)); - this._terminalProcesses[id].onProcessTitleChanged(title => this._proxy.$sendProcessTitle(id, title)); - this._terminalProcesses[id].onProcessData(data => this._proxy.$sendProcessData(id, data)); - this._terminalProcesses[id].onProcessExit((exitCode) => this._onProcessExit(id, exitCode)); + const p = new TerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, terminalConfig.get('windowsEnableConpty')); + p.onProcessIdReady(pid => this._proxy.$sendProcessPid(id, pid)); + p.onProcessTitleChanged(title => this._proxy.$sendProcessTitle(id, title)); + p.onProcessData(data => this._proxy.$sendProcessData(id, data)); + p.onProcessExit((exitCode) => this._onProcessExit(id, exitCode)); + this._terminalProcesses[id] = p; } @@ -503,6 +506,14 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { this._terminalProcesses[id].shutdown(immediate); } + public $acceptProcessRequestInitialCwd(id: number): void { + this._terminalProcesses[id].getInitialCwd().then(initialCwd => this._proxy.$sendProcessInitialCwd(id, initialCwd)); + } + + public $acceptProcessRequestCwd(id: number): void { + this._terminalProcesses[id].getCwd().then(cwd => this._proxy.$sendProcessCwd(id, cwd)); + } + private _onProcessExit(id: number, exitCode: number): void { // Remove listeners this._terminalProcesses[id].dispose(); @@ -639,8 +650,11 @@ class CLIServer { req.setEncoding('utf8'); req.on('data', (d: string) => chunks.push(d)); req.on('end', () => { - const { fileURIs, folderURIs, forceNewWindow, diffMode, addMode, forceReuseWindow } = JSON.parse(chunks.join('')); + let { fileURIs, folderURIs, forceNewWindow, diffMode, addMode, forceReuseWindow } = JSON.parse(chunks.join('')); if (folderURIs && folderURIs.length || fileURIs && fileURIs.length) { + if (folderURIs && folderURIs.length && !forceReuseWindow) { + forceNewWindow = true; + } this._commands.executeCommand('_files.windowOpen', { folderURIs: this.toURIs(folderURIs), fileURIs: this.toURIs(fileURIs), forceNewWindow, diffMode, addMode, forceReuseWindow }); } res.writeHead(200); diff --git a/src/vs/workbench/api/shared/tasks.ts b/src/vs/workbench/api/shared/tasks.ts index e7a392ac0c3..fb58258c443 100644 --- a/src/vs/workbench/api/shared/tasks.ts +++ b/src/vs/workbench/api/shared/tasks.ts @@ -18,6 +18,7 @@ export interface TaskPresentationOptionsDTO { panel?: number; showReuseMessage?: boolean; clear?: boolean; + group?: string; } export interface RunOptionsDTO { diff --git a/src/vs/workbench/browser/actions.ts b/src/vs/workbench/browser/actions.ts index 628fd519627..c6355e52f00 100644 --- a/src/vs/workbench/browser/actions.ts +++ b/src/vs/workbench/browser/actions.ts @@ -54,6 +54,7 @@ export class ActionBarContributor { * Some predefined scopes to contribute actions to */ export const Scope = { + /** * Actions inside tree widgets. */ diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts new file mode 100644 index 00000000000..70bafe35ada --- /dev/null +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -0,0 +1,510 @@ +/*--------------------------------------------------------------------------------------------- + * 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/actions'; + +import * as nls from 'vs/nls'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Action } from 'vs/base/common/actions'; +import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; +import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IPartService, Parts, Position } from 'vs/workbench/services/part/common/partService'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IEditorGroupsService, GroupOrientation } from 'vs/workbench/services/group/common/editorGroupsService'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { MenuBarVisibility } from 'vs/platform/windows/common/windows'; +import { isWindows, isLinux } from 'vs/base/common/platform'; +import { IsMacContext } from 'vs/platform/workbench/common/contextkeys'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { InEditorZenModeContext } from 'vs/workbench/common/editor'; + +const registry = Registry.as(Extensions.WorkbenchActions); +const viewCategory = nls.localize('view', "View"); + +// --- Toggle Activity Bar + +export class ToggleActivityBarVisibilityAction extends Action { + + static readonly ID = 'workbench.action.toggleActivityBarVisibility'; + static readonly LABEL = nls.localize('toggleActivityBar', "Toggle Activity Bar Visibility"); + + private static readonly activityBarVisibleKey = 'workbench.activityBar.visible'; + + constructor( + id: string, + label: string, + @IPartService private readonly partService: IPartService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(id, label); + + this.enabled = !!this.partService; + } + + run(): Promise { + const visibility = this.partService.isVisible(Parts.ACTIVITYBAR_PART); + const newVisibilityValue = !visibility; + + return this.configurationService.updateValue(ToggleActivityBarVisibilityAction.activityBarVisibleKey, newVisibilityValue, ConfigurationTarget.USER); + } +} + +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleActivityBarVisibilityAction, ToggleActivityBarVisibilityAction.ID, ToggleActivityBarVisibilityAction.LABEL), 'View: Toggle Activity Bar Visibility', viewCategory); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '2_workbench_layout', + command: { + id: ToggleActivityBarVisibilityAction.ID, + title: nls.localize({ key: 'miToggleActivityBar', comment: ['&& denotes a mnemonic'] }, "Toggle &&Activity Bar") + }, + order: 4 +}); + +// --- Toggle Centered Layout + +class ToggleCenteredLayout extends Action { + + static readonly ID = 'workbench.action.toggleCenteredLayout'; + static readonly LABEL = nls.localize('toggleCenteredLayout', "Toggle Centered Layout"); + + constructor( + id: string, + label: string, + @IPartService private readonly partService: IPartService + ) { + super(id, label); + this.enabled = !!this.partService; + } + + run(): Promise { + this.partService.centerEditorLayout(!this.partService.isEditorLayoutCentered()); + + return Promise.resolve(null); + } +} + +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleCenteredLayout, ToggleCenteredLayout.ID, ToggleCenteredLayout.LABEL), 'View: Toggle Centered Layout', viewCategory); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '1_toggle_view', + command: { + id: ToggleCenteredLayout.ID, + title: nls.localize('miToggleCenteredLayout', "Toggle Centered Layout") + }, + 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 toDispose: IDisposable[]; + + constructor( + id: string, + label: string, + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService + ) { + super(id, label); + + this.toDispose = []; + + this.class = 'flip-editor-layout'; + this.updateEnablement(); + + this.registerListeners(); + } + + private registerListeners(): void { + this.toDispose.push(this.editorGroupService.onDidAddGroup(() => this.updateEnablement())); + this.toDispose.push(this.editorGroupService.onDidRemoveGroup(() => this.updateEnablement())); + } + + private updateEnablement(): void { + this.enabled = this.editorGroupService.count > 1; + } + + run(): Promise { + const newOrientation = (this.editorGroupService.orientation === GroupOrientation.VERTICAL) ? GroupOrientation.HORIZONTAL : GroupOrientation.VERTICAL; + this.editorGroupService.setGroupOrientation(newOrientation); + + return Promise.resolve(null); + } + + dispose(): void { + this.toDispose = dispose(this.toDispose); + + super.dispose(); + } +} + +CommandsRegistry.registerCommand('_workbench.editor.setGroupOrientation', function (accessor: ServicesAccessor, args: [GroupOrientation]) { + const editorGroupService = accessor.get(IEditorGroupsService); + const [orientation] = args; + + editorGroupService.setGroupOrientation(orientation); + + return Promise.resolve(null); +}); + +const group = viewCategory; +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleEditorLayoutAction, ToggleEditorLayoutAction.ID, ToggleEditorLayoutAction.LABEL, { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_0, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_0 } }), 'View: Flip Editor Group Layout', group); + +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 { + + static readonly ID = 'workbench.action.toggleSidebarPosition'; + static readonly LABEL = nls.localize('toggleSidebarPosition', "Toggle Side Bar Position"); + + private static readonly sidebarPositionConfigurationKey = 'workbench.sideBar.location'; + + constructor( + id: string, + label: string, + @IPartService private readonly partService: IPartService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(id, label); + + this.enabled = !!this.partService && !!this.configurationService; + } + + run(): Promise { + const position = this.partService.getSideBarPosition(); + const newPositionValue = (position === Position.LEFT) ? 'right' : 'left'; + + return this.configurationService.updateValue(ToggleSidebarPositionAction.sidebarPositionConfigurationKey, newPositionValue, ConfigurationTarget.USER); + } + + static getLabel(partService: IPartService): string { + return partService.getSideBarPosition() === Position.LEFT ? nls.localize('moveSidebarRight', "Move Side Bar Right") : nls.localize('moveSidebarLeft', "Move Side Bar Left"); + } +} + +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.LABEL), 'View: Toggle Side Bar Position', viewCategory); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '2_workbench_layout', + command: { + id: ToggleSidebarPositionAction.ID, + title: nls.localize({ key: 'miMoveSidebarLeftRight', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Left/Right") + }, + order: 2 +}); + +// --- Toggle Sidebar Visibility + +export class ToggleEditorVisibilityAction extends Action { + static readonly ID = 'workbench.action.toggleEditorVisibility'; + static readonly LABEL = nls.localize('toggleEditor', "Toggle Editor Area"); + + constructor( + id: string, + label: string, + @IPartService private readonly partService: IPartService + ) { + super(id, label); + + this.enabled = !!this.partService; + } + + run(): Promise { + const hideEditor = this.partService.isVisible(Parts.EDITOR_PART); + this.partService.setEditorHidden(hideEditor); + + return Promise.resolve(null); + } + +} + +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleEditorVisibilityAction, ToggleEditorVisibilityAction.ID, ToggleEditorVisibilityAction.LABEL), 'View: Toggle Editor Area Visibility', viewCategory); + + +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, + @IPartService private readonly partService: IPartService + ) { + super(id, label); + + this.enabled = !!this.partService; + } + + run(): Promise { + const hideSidebar = this.partService.isVisible(Parts.SIDEBAR_PART); + this.partService.setSideBarHidden(hideSidebar); + + return Promise.resolve(null); + } +} + +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleSidebarVisibilityAction, ToggleSidebarVisibilityAction.ID, ToggleSidebarVisibilityAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_B }), 'View: Toggle Side Bar Visibility', viewCategory); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '2_workbench_layout', + command: { + id: ToggleSidebarVisibilityAction.ID, + title: nls.localize({ key: 'miToggleSidebar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Side Bar") + }, + order: 1 +}); + +// --- Toggle Statusbar Visibility + +class ToggleStatusbarVisibilityAction extends Action { + + static readonly ID = 'workbench.action.toggleStatusbarVisibility'; + static readonly LABEL = nls.localize('toggleStatusbar', "Toggle Status Bar Visibility"); + + private static readonly statusbarVisibleKey = 'workbench.statusBar.visible'; + + constructor( + id: string, + label: string, + @IPartService private readonly partService: IPartService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(id, label); + + this.enabled = !!this.partService; + } + + run(): Promise { + const visibility = this.partService.isVisible(Parts.STATUSBAR_PART); + const newVisibilityValue = !visibility; + + return this.configurationService.updateValue(ToggleStatusbarVisibilityAction.statusbarVisibleKey, newVisibilityValue, ConfigurationTarget.USER); + } +} + +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleStatusbarVisibilityAction, ToggleStatusbarVisibilityAction.ID, ToggleStatusbarVisibilityAction.LABEL), 'View: Toggle Status Bar Visibility', viewCategory); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '2_workbench_layout', + command: { + id: ToggleStatusbarVisibilityAction.ID, + title: nls.localize({ key: 'miToggleStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Status Bar") + }, + order: 3 +}); + +// --- Toggle Tabs Visibility + +class ToggleTabsVisibilityAction extends Action { + + static readonly ID = 'workbench.action.toggleTabsVisibility'; + static readonly LABEL = nls.localize('toggleTabs', "Toggle Tab Visibility"); + + private static readonly tabsVisibleKey = 'workbench.editor.showTabs'; + + constructor( + id: string, + label: string, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(id, label); + } + + run(): Promise { + const visibility = this.configurationService.getValue(ToggleTabsVisibilityAction.tabsVisibleKey); + const newVisibilityValue = !visibility; + + return this.configurationService.updateValue(ToggleTabsVisibilityAction.tabsVisibleKey, newVisibilityValue); + } +} + +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleTabsVisibilityAction, ToggleTabsVisibilityAction.ID, ToggleTabsVisibilityAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_W }), 'View: Toggle Tab Visibility', viewCategory); + +// --- Toggle Zen Mode + +class ToggleZenMode extends Action { + + static readonly ID = 'workbench.action.toggleZenMode'; + static readonly LABEL = nls.localize('toggleZenMode', "Toggle Zen Mode"); + + constructor( + id: string, + label: string, + @IPartService private readonly partService: IPartService + ) { + super(id, label); + this.enabled = !!this.partService; + } + + run(): Promise { + this.partService.toggleZenMode(); + + return Promise.resolve(null); + } +} + +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleZenMode, ToggleZenMode.ID, ToggleZenMode.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_Z) }), 'View: Toggle Zen Mode', viewCategory); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '1_toggle_view', + command: { + id: ToggleZenMode.ID, + title: nls.localize('miToggleZenMode', "Toggle Zen Mode") + }, + order: 2 +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.action.exitZenMode', + weight: KeybindingWeight.EditorContrib - 1000, + handler(accessor: ServicesAccessor) { + const partService = accessor.get(IPartService); + partService.toggleZenMode(); + }, + when: InEditorZenModeContext, + primary: KeyChord(KeyCode.Escape, KeyCode.Escape) +}); + +// --- Toggle Menu Bar + +export class ToggleMenuBarAction extends Action { + + static readonly ID = 'workbench.action.toggleMenuBar'; + static LABEL = nls.localize('toggleMenuBar', "Toggle Menu Bar"); + + private static readonly menuBarVisibilityKey = 'window.menuBarVisibility'; + + constructor( + id: string, + label: string, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(id, label); + } + + run(): Promise { + let currentVisibilityValue = this.configurationService.getValue(ToggleMenuBarAction.menuBarVisibilityKey); + if (typeof currentVisibilityValue !== 'string') { + currentVisibilityValue = 'default'; + } + + let newVisibilityValue: string; + if (currentVisibilityValue === 'visible' || currentVisibilityValue === 'default') { + newVisibilityValue = 'toggle'; + } else { + newVisibilityValue = 'default'; + } + + this.configurationService.updateValue(ToggleMenuBarAction.menuBarVisibilityKey, newVisibilityValue, ConfigurationTarget.USER); + + return Promise.resolve(); + } +} + +if (isWindows || isLinux) { + registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleMenuBarAction, ToggleMenuBarAction.ID, ToggleMenuBarAction.LABEL), 'View: Toggle Menu Bar', viewCategory); +} + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '1_toggle_view', + command: { + id: ToggleMenuBarAction.ID, + title: nls.localize({ key: 'miToggleMenuBar', comment: ['&& denotes a mnemonic'] }, "Toggle Menu &&Bar") + }, + when: IsMacContext.toNegated(), + order: 4 +}); + +// --- Resize View + +export abstract class BaseResizeViewAction extends Action { + + protected static RESIZE_INCREMENT = 6.5; // This is a media-size percentage + + constructor( + id: string, + label: string, + @IPartService protected partService: IPartService + ) { + super(id, label); + } + + protected resizePart(sizeChange: number): void { + const isEditorFocus = this.partService.hasFocus(Parts.EDITOR_PART); + const isSidebarFocus = this.partService.hasFocus(Parts.SIDEBAR_PART); + const isPanelFocus = this.partService.hasFocus(Parts.PANEL_PART); + + let part: Parts | undefined; + if (isSidebarFocus) { + part = Parts.SIDEBAR_PART; + } else if (isPanelFocus) { + part = Parts.PANEL_PART; + } else if (isEditorFocus) { + part = Parts.EDITOR_PART; + } + + if (part) { + this.partService.resizePart(part, sizeChange); + } + } +} + +export class IncreaseViewSizeAction extends BaseResizeViewAction { + + static readonly ID = 'workbench.action.increaseViewSize'; + static readonly LABEL = nls.localize('increaseViewSize', "Increase Current View Size"); + + constructor( + id: string, + label: string, + @IPartService partService: IPartService + ) { + super(id, label, partService); + } + + run(): Promise { + this.resizePart(BaseResizeViewAction.RESIZE_INCREMENT); + return Promise.resolve(true); + } +} + +export class DecreaseViewSizeAction extends BaseResizeViewAction { + + static readonly ID = 'workbench.action.decreaseViewSize'; + static readonly LABEL = nls.localize('decreaseViewSize', "Decrease Current View Size"); + + constructor( + id: string, + label: string, + @IPartService partService: IPartService + + ) { + super(id, label, partService); + } + + run(): Promise { + this.resizePart(-BaseResizeViewAction.RESIZE_INCREMENT); + return Promise.resolve(true); + } +} + +registry.registerWorkbenchAction(new SyncActionDescriptor(IncreaseViewSizeAction, IncreaseViewSizeAction.ID, IncreaseViewSizeAction.LABEL, undefined), 'View: Increase Current View Size', viewCategory); +registry.registerWorkbenchAction(new SyncActionDescriptor(DecreaseViewSizeAction, DecreaseViewSizeAction.ID, DecreaseViewSizeAction.LABEL, undefined), 'View: Decrease Current View Size', viewCategory); diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts new file mode 100644 index 00000000000..e5ee5abd4e3 --- /dev/null +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -0,0 +1,736 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { List } from 'vs/base/browser/ui/list/listWidget'; +import { WorkbenchListFocusContextKey, IListService, WorkbenchListSupportsMultiSelectContextKey, ListWidget, WorkbenchListHasSelectionOrFocus } from 'vs/platform/list/browser/listService'; +import { PagedList } from 'vs/base/browser/ui/list/listPaging'; +import { range } from 'vs/base/common/arrays'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ITree } from 'vs/base/parts/tree/browser/tree'; +import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; +import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; + +function ensureDOMFocus(widget: ListWidget): void { + // it can happen that one of the commands is executed while + // DOM focus is within another focusable control within the + // list/tree item. therefor we should ensure that the + // list/tree has DOM focus again after the command ran. + if (widget && widget.getHTMLElement() !== document.activeElement) { + widget.domFocus(); + } +} + +function focusDown(accessor: ServicesAccessor, arg2?: number, loop: boolean = true): void { + const focused = accessor.get(IListService).lastFocusedList; + const count = typeof arg2 === 'number' ? arg2 : 1; + + // Ensure DOM Focus + ensureDOMFocus(focused); + + // List + if (focused instanceof List || focused instanceof PagedList) { + const list = focused; + + list.focusNext(count); + const listFocus = list.getFocus(); + if (listFocus.length) { + list.reveal(listFocus[0]); + } + } + + // ObjectTree + else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const tree = focused; + + const fakeKeyboardEvent = new KeyboardEvent('keydown'); + tree.focusNext(count, loop, fakeKeyboardEvent); + + const listFocus = tree.getFocus(); + if (listFocus.length) { + tree.reveal(listFocus[0]); + } + } + + // Tree + else if (focused) { + const tree = focused; + + tree.focusNext(count, { origin: 'keyboard' }); + tree.reveal(tree.getFocus()); + } +} + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.focusDown', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: KeyCode.DownArrow, + mac: { + primary: KeyCode.DownArrow, + secondary: [KeyMod.WinCtrl | KeyCode.KEY_N] + }, + handler: (accessor, arg2) => focusDown(accessor, arg2) +}); + +function expandMultiSelection(focused: List | PagedList | ITree | ObjectTree | DataTree | AsyncDataTree, previousFocus: any): void { + + // List + if (focused instanceof List || focused instanceof PagedList) { + const list = focused; + + const focus = list.getFocus() ? list.getFocus()[0] : undefined; + const selection = list.getSelection(); + if (selection && selection.indexOf(focus) >= 0) { + list.setSelection(selection.filter(s => s !== previousFocus)); + } else { + list.setSelection(selection.concat(focus)); + } + } + + // ObjectTree + else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const list = focused; + + const focus = list.getFocus() ? list.getFocus()[0] : undefined; + + if (previousFocus === focus) { + return; + } + + const selection = list.getSelection(); + const fakeKeyboardEvent = new KeyboardEvent('keydown', { shiftKey: true }); + + if (selection && selection.indexOf(focus) >= 0) { + list.setSelection(selection.filter(s => s !== previousFocus), fakeKeyboardEvent); + } else { + list.setSelection(selection.concat(focus), fakeKeyboardEvent); + } + } + + // Tree + else if (focused) { + const tree = focused; + + const focus = tree.getFocus(); + const selection = tree.getSelection(); + if (selection && selection.indexOf(focus) >= 0) { + tree.setSelection(selection.filter(s => s !== previousFocus)); + } else { + tree.setSelection(selection.concat(focus)); + } + } +} + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.expandSelectionDown', + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListSupportsMultiSelectContextKey), + primary: KeyMod.Shift | KeyCode.DownArrow, + handler: (accessor, arg2) => { + const focused = accessor.get(IListService).lastFocusedList; + + // List + if (focused instanceof List || focused instanceof PagedList || focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const list = focused; + + // Focus down first + const previousFocus = list.getFocus() ? list.getFocus()[0] : undefined; + focusDown(accessor, arg2, false); + + // Then adjust selection + expandMultiSelection(focused, previousFocus); + } + + // Tree + else if (focused) { + const tree = focused; + + // Focus down first + const previousFocus = tree.getFocus(); + focusDown(accessor, arg2); + + // Then adjust selection + expandMultiSelection(focused, previousFocus); + } + } +}); + +function focusUp(accessor: ServicesAccessor, arg2?: number, loop: boolean = true): void { + const focused = accessor.get(IListService).lastFocusedList; + const count = typeof arg2 === 'number' ? arg2 : 1; + + // Ensure DOM Focus + ensureDOMFocus(focused); + + // List + if (focused instanceof List || focused instanceof PagedList) { + const list = focused; + + list.focusPrevious(count); + const listFocus = list.getFocus(); + if (listFocus.length) { + list.reveal(listFocus[0]); + } + } + + // ObjectTree + else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const tree = focused; + + const fakeKeyboardEvent = new KeyboardEvent('keydown'); + tree.focusPrevious(count, loop, fakeKeyboardEvent); + + const listFocus = tree.getFocus(); + if (listFocus.length) { + tree.reveal(listFocus[0]); + } + } + + // Tree + else if (focused) { + const tree = focused; + + tree.focusPrevious(count, { origin: 'keyboard' }); + tree.reveal(tree.getFocus()); + } +} + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.focusUp', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: KeyCode.UpArrow, + mac: { + primary: KeyCode.UpArrow, + secondary: [KeyMod.WinCtrl | KeyCode.KEY_P] + }, + handler: (accessor, arg2) => focusUp(accessor, arg2) +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.expandSelectionUp', + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListSupportsMultiSelectContextKey), + primary: KeyMod.Shift | KeyCode.UpArrow, + handler: (accessor, arg2) => { + const focused = accessor.get(IListService).lastFocusedList; + + // List + if (focused instanceof List || focused instanceof PagedList || focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const list = focused; + + // Focus up first + const previousFocus = list.getFocus() ? list.getFocus()[0] : undefined; + focusUp(accessor, arg2, false); + + // Then adjust selection + expandMultiSelection(focused, previousFocus); + } + + // Tree + else if (focused) { + const tree = focused; + + // Focus up first + const previousFocus = tree.getFocus(); + focusUp(accessor, arg2); + + // Then adjust selection + expandMultiSelection(focused, previousFocus); + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.collapse', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: KeyCode.LeftArrow, + mac: { + primary: KeyCode.LeftArrow, + secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow] + }, + handler: (accessor) => { + const focused = accessor.get(IListService).lastFocusedList; + + // Tree only + if (focused && !(focused instanceof List || focused instanceof PagedList)) { + if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const tree = focused; + const focusedElements = tree.getFocus(); + + if (focusedElements.length === 0) { + return; + } + + const focus = focusedElements[0]; + + if (!tree.collapse(focus)) { + const parent = tree.getParentElement(focus); + + if (parent) { + const fakeKeyboardEvent = new KeyboardEvent('keydown'); + tree.setFocus([parent], fakeKeyboardEvent); + tree.reveal(parent); + } + } + } else { + const tree = focused; + const focus = tree.getFocus(); + + tree.collapse(focus).then(didCollapse => { + if (focus && !didCollapse) { + tree.focusParent({ origin: 'keyboard' }); + + return tree.reveal(tree.getFocus()); + } + + return undefined; + }); + } + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.expand', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: KeyCode.RightArrow, + handler: (accessor) => { + const focused = accessor.get(IListService).lastFocusedList; + + // Tree only + if (focused && !(focused instanceof List || focused instanceof PagedList)) { + if (focused instanceof ObjectTree || focused instanceof DataTree) { + const tree = focused; + const focusedElements = tree.getFocus(); + + if (focusedElements.length === 0) { + return; + } + + const focus = focusedElements[0]; + + if (!tree.expand(focus)) { + const child = tree.getFirstElementChild(focus); + + if (child) { + const fakeKeyboardEvent = new KeyboardEvent('keydown'); + tree.setFocus([child], fakeKeyboardEvent); + tree.reveal(child); + } + } + } else if (focused instanceof AsyncDataTree) { + const tree = focused; + const focusedElements = tree.getFocus(); + + if (focusedElements.length === 0) { + return; + } + + const focus = focusedElements[0]; + tree.expand(focus).then(didExpand => { + if (focus && !didExpand) { + const child = tree.getFirstElementChild(focus); + + if (child) { + const fakeKeyboardEvent = new KeyboardEvent('keydown'); + tree.setFocus([child], fakeKeyboardEvent); + tree.reveal(child); + } + } + }); + } else { + const tree = focused; + const focus = tree.getFocus(); + + tree.expand(focus).then(didExpand => { + if (focus && !didExpand) { + tree.focusFirstChild({ origin: 'keyboard' }); + + return tree.reveal(tree.getFocus()); + } + + return undefined; + }); + } + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.focusPageUp', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: KeyCode.PageUp, + handler: (accessor) => { + const focused = accessor.get(IListService).lastFocusedList; + + // Ensure DOM Focus + ensureDOMFocus(focused); + + // List + if (focused instanceof List || focused instanceof PagedList) { + const list = focused; + + list.focusPreviousPage(); + list.reveal(list.getFocus()[0]); + } + + // ObjectTree + else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const list = focused; + + const fakeKeyboardEvent = new KeyboardEvent('keydown'); + list.focusPreviousPage(fakeKeyboardEvent); + list.reveal(list.getFocus()[0]); + } + + // Tree + else if (focused) { + const tree = focused; + + tree.focusPreviousPage({ origin: 'keyboard' }); + tree.reveal(tree.getFocus()); + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.focusPageDown', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: KeyCode.PageDown, + handler: (accessor) => { + const focused = accessor.get(IListService).lastFocusedList; + + // Ensure DOM Focus + ensureDOMFocus(focused); + + // List + if (focused instanceof List || focused instanceof PagedList) { + const list = focused; + + list.focusNextPage(); + list.reveal(list.getFocus()[0]); + } + + // ObjectTree + else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const list = focused; + + const fakeKeyboardEvent = new KeyboardEvent('keydown'); + list.focusNextPage(fakeKeyboardEvent); + list.reveal(list.getFocus()[0]); + } + + // Tree + else if (focused) { + const tree = focused; + + tree.focusNextPage({ origin: 'keyboard' }); + tree.reveal(tree.getFocus()); + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.focusFirst', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: KeyCode.Home, + handler: accessor => listFocusFirst(accessor) +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.focusFirstChild', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: 0, + handler: accessor => listFocusFirst(accessor, { fromFocused: true }) +}); + +function listFocusFirst(accessor: ServicesAccessor, options?: { fromFocused: boolean }): void { + const focused = accessor.get(IListService).lastFocusedList; + + // Ensure DOM Focus + ensureDOMFocus(focused); + + // List + if (focused instanceof List || focused instanceof PagedList) { + const list = focused; + + list.setFocus([0]); + list.reveal(0); + } + + // ObjectTree + else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const tree = focused; + const fakeKeyboardEvent = new KeyboardEvent('keydown'); + tree.focusFirst(fakeKeyboardEvent); + + const focus = tree.getFocus(); + + if (focus.length > 0) { + tree.reveal(focus[0]); + } + } + + // Tree + else if (focused) { + const tree = focused; + + tree.focusFirst({ origin: 'keyboard' }, options && options.fromFocused ? tree.getFocus() : undefined); + tree.reveal(tree.getFocus()); + } +} + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.focusLast', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: KeyCode.End, + handler: accessor => listFocusLast(accessor) +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.focusLastChild', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: 0, + handler: accessor => listFocusLast(accessor, { fromFocused: true }) +}); + +function listFocusLast(accessor: ServicesAccessor, options?: { fromFocused: boolean }): void { + const focused = accessor.get(IListService).lastFocusedList; + + // Ensure DOM Focus + ensureDOMFocus(focused); + + // List + if (focused instanceof List || focused instanceof PagedList) { + const list = focused; + + list.setFocus([list.length - 1]); + list.reveal(list.length - 1); + } + + // ObjectTree + else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const tree = focused; + const fakeKeyboardEvent = new KeyboardEvent('keydown'); + tree.focusLast(fakeKeyboardEvent); + + const focus = tree.getFocus(); + + if (focus.length > 0) { + tree.reveal(focus[0]); + } + } + + // Tree + else if (focused) { + const tree = focused; + + tree.focusLast({ origin: 'keyboard' }, options && options.fromFocused ? tree.getFocus() : undefined); + tree.reveal(tree.getFocus()); + } +} + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.select', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: KeyCode.Enter, + mac: { + primary: KeyCode.Enter, + secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow] + }, + handler: (accessor) => { + const focused = accessor.get(IListService).lastFocusedList; + + // List + if (focused instanceof List || focused instanceof PagedList) { + const list = focused; + list.setSelection(list.getFocus()); + list.open(list.getFocus()); + } + + // ObjectTree + else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const list = focused; + const fakeKeyboardEvent = new KeyboardEvent('keydown'); + list.setSelection(list.getFocus(), fakeKeyboardEvent); + list.open(list.getFocus()); + } + + // Tree + else if (focused) { + const tree = focused; + const focus = tree.getFocus(); + + if (focus) { + tree.setSelection([focus], { origin: 'keyboard' }); + } + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.selectAll', + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListSupportsMultiSelectContextKey), + primary: KeyMod.CtrlCmd | KeyCode.KEY_A, + handler: (accessor) => { + const focused = accessor.get(IListService).lastFocusedList; + + // List + if (focused instanceof List || focused instanceof PagedList) { + const list = focused; + list.setSelection(range(list.length)); + } + + // Trees + else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const tree = focused; + const focus = tree.getFocus(); + const selection = tree.getSelection(); + + // Which element should be considered to start selecting all? + let start: any | undefined = undefined; + + if (focus.length > 0 && (selection.length === 0 || selection.indexOf(focus[0]) === -1)) { + start = focus[0]; + } + + if (!start && selection.length > 0) { + start = selection[0]; + } + + // What is the scope of select all? + let scope: any | undefined = undefined; + + if (!start) { + scope = undefined; + } else { + const selectedNode = tree.getNode(start); + const parentNode = selectedNode.parent; + + if (!parentNode.parent) { // root + scope = undefined; + } else { + scope = parentNode.element; + } + } + + const newSelection: any[] = []; + + // If the scope isn't the tree root, it should be part of the new selection + if (scope) { + newSelection.push(scope); + } + + const visit = (node: ITreeNode) => { + for (const child of node.children) { + if (child.visible) { + newSelection.push(child.element); + + if (!child.collapsed) { + visit(child); + } + } + } + }; + + // Add the whole scope subtree to the new selection + visit(tree.getNode(scope)); + + const fakeKeyboardEvent = new KeyboardEvent('keydown'); + tree.setSelection(newSelection, fakeKeyboardEvent); + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.toggleExpand', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: KeyCode.Space, + handler: (accessor) => { + const focused = accessor.get(IListService).lastFocusedList; + + // Tree only + if (focused && !(focused instanceof List || focused instanceof PagedList)) { + if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const tree = focused; + const focus = tree.getFocus(); + + if (focus.length === 0) { + return; + } + + tree.toggleCollapsed(focus[0]); + } else { + const tree = focused; + const focus = tree.getFocus(); + + if (focus) { + tree.toggleExpansion(focus); + } + } + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.clear', + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListHasSelectionOrFocus), + primary: KeyCode.Escape, + handler: (accessor) => { + const focused = accessor.get(IListService).lastFocusedList; + + // List + if (focused instanceof List || focused instanceof PagedList) { + const list = focused; + + if (list.getSelection().length > 0) { + list.setSelection([]); + } else if (list.getFocus().length > 0) { + list.setFocus([]); + } + } + + // ObjectTree + else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const list = focused; + const fakeKeyboardEvent = new KeyboardEvent('keydown'); + + if (list.getSelection().length > 0) { + list.setSelection([], fakeKeyboardEvent); + } else if (list.getFocus().length > 0) { + list.setFocus([], fakeKeyboardEvent); + } + } + + // Tree + else if (focused) { + const tree = focused; + + if (tree.getSelection().length) { + tree.clearSelection({ origin: 'keyboard' }); + } else if (tree.getFocus()) { + tree.clearFocus({ origin: 'keyboard' }); + } + } + } +}); \ No newline at end of file diff --git a/src/vs/workbench/browser/actions/navigationActions.ts b/src/vs/workbench/browser/actions/navigationActions.ts new file mode 100644 index 00000000000..6a83374bacb --- /dev/null +++ b/src/vs/workbench/browser/actions/navigationActions.ts @@ -0,0 +1,272 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Registry } from 'vs/platform/registry/common/platform'; +import { Action } from 'vs/base/common/actions'; +import { IEditorGroupsService, GroupDirection, GroupLocation, IFindGroupScope } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; +import { IPartService, Parts, Position as PartPosition } from 'vs/workbench/services/part/common/partService'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { IViewlet } from 'vs/workbench/common/viewlet'; +import { IPanel } from 'vs/workbench/common/panel'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; + +abstract class BaseNavigationAction extends Action { + + constructor( + id: string, + label: string, + @IEditorGroupsService protected editorGroupService: IEditorGroupsService, + @IPanelService protected panelService: IPanelService, + @IPartService protected partService: IPartService, + @IViewletService protected viewletService: IViewletService + ) { + super(id, label); + } + + run(): Promise { + const isEditorFocus = this.partService.hasFocus(Parts.EDITOR_PART); + const isPanelFocus = this.partService.hasFocus(Parts.PANEL_PART); + const isSidebarFocus = this.partService.hasFocus(Parts.SIDEBAR_PART); + + const isSidebarPositionLeft = this.partService.getSideBarPosition() === PartPosition.LEFT; + const isPanelPositionDown = this.partService.getPanelPosition() === PartPosition.BOTTOM; + + if (isEditorFocus) { + return this.navigateOnEditorFocus(isSidebarPositionLeft, isPanelPositionDown); + } + + if (isPanelFocus) { + return this.navigateOnPanelFocus(isSidebarPositionLeft, isPanelPositionDown); + } + + if (isSidebarFocus) { + return Promise.resolve(this.navigateOnSidebarFocus(isSidebarPositionLeft, isPanelPositionDown)); + } + + return Promise.resolve(false); + } + + protected navigateOnEditorFocus(_isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): Promise { + return Promise.resolve(true); + } + + protected navigateOnPanelFocus(_isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): Promise { + return Promise.resolve(true); + } + + protected navigateOnSidebarFocus(_isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): boolean | IViewlet { + return true; + } + + protected navigateToPanel(): IPanel | boolean { + if (!this.partService.isVisible(Parts.PANEL_PART)) { + return false; + } + + const activePanelId = this.panelService.getActivePanel().getId(); + + return this.panelService.openPanel(activePanelId, true); + } + + protected navigateToSidebar(): Promise { + if (!this.partService.isVisible(Parts.SIDEBAR_PART)) { + return Promise.resolve(false); + } + + const activeViewletId = this.viewletService.getActiveViewlet().getId(); + + return this.viewletService.openViewlet(activeViewletId, true) + .then(value => value === null ? false : value); + } + + protected navigateAcrossEditorGroup(direction: GroupDirection): boolean { + return this.doNavigateToEditorGroup({ direction }); + } + + protected navigateToEditorGroup(location: GroupLocation): boolean { + return this.doNavigateToEditorGroup({ location }); + } + + private doNavigateToEditorGroup(scope: IFindGroupScope): boolean { + const targetGroup = this.editorGroupService.findGroup(scope, this.editorGroupService.activeGroup); + if (targetGroup) { + targetGroup.focus(); + + return true; + } + + return false; + } +} + +class NavigateLeftAction extends BaseNavigationAction { + + static readonly ID = 'workbench.action.navigateLeft'; + static readonly LABEL = nls.localize('navigateLeft', "Navigate to the View on the Left"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IPanelService panelService: IPanelService, + @IPartService partService: IPartService, + @IViewletService viewletService: IViewletService + ) { + super(id, label, editorGroupService, panelService, partService, viewletService); + } + + protected navigateOnEditorFocus(isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): Promise { + const didNavigate = this.navigateAcrossEditorGroup(GroupDirection.LEFT); + if (didNavigate) { + return Promise.resolve(true); + } + + if (isSidebarPositionLeft) { + return this.navigateToSidebar(); + } + + return Promise.resolve(false); + } + + protected navigateOnPanelFocus(isSidebarPositionLeft: boolean, isPanelPositionDown: boolean): Promise { + if (isPanelPositionDown && isSidebarPositionLeft) { + return this.navigateToSidebar(); + } + + if (!isPanelPositionDown) { + return Promise.resolve(this.navigateToEditorGroup(GroupLocation.LAST)); + } + + return Promise.resolve(false); + } + + protected navigateOnSidebarFocus(isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): boolean { + if (!isSidebarPositionLeft) { + return this.navigateToEditorGroup(GroupLocation.LAST); + } + + return false; + } +} + +class NavigateRightAction extends BaseNavigationAction { + + static readonly ID = 'workbench.action.navigateRight'; + static readonly LABEL = nls.localize('navigateRight', "Navigate to the View on the Right"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IPanelService panelService: IPanelService, + @IPartService partService: IPartService, + @IViewletService viewletService: IViewletService + ) { + super(id, label, editorGroupService, panelService, partService, viewletService); + } + + protected navigateOnEditorFocus(isSidebarPositionLeft: boolean, isPanelPositionDown: boolean): Promise { + const didNavigate = this.navigateAcrossEditorGroup(GroupDirection.RIGHT); + if (didNavigate) { + return Promise.resolve(true); + } + + if (!isPanelPositionDown) { + return Promise.resolve(this.navigateToPanel()); + } + + if (!isSidebarPositionLeft) { + return this.navigateToSidebar(); + } + + return Promise.resolve(false); + } + + protected navigateOnPanelFocus(isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): Promise { + if (!isSidebarPositionLeft) { + return this.navigateToSidebar(); + } + + return Promise.resolve(false); + } + + protected navigateOnSidebarFocus(isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): boolean { + if (isSidebarPositionLeft) { + return this.navigateToEditorGroup(GroupLocation.FIRST); + } + + return false; + } +} + +class NavigateUpAction extends BaseNavigationAction { + + static readonly ID = 'workbench.action.navigateUp'; + static readonly LABEL = nls.localize('navigateUp', "Navigate to the View Above"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IPanelService panelService: IPanelService, + @IPartService partService: IPartService, + @IViewletService viewletService: IViewletService + ) { + super(id, label, editorGroupService, panelService, partService, viewletService); + } + + protected navigateOnEditorFocus(_isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): Promise { + return Promise.resolve(this.navigateAcrossEditorGroup(GroupDirection.UP)); + } + + protected navigateOnPanelFocus(_isSidebarPositionLeft: boolean, isPanelPositionDown: boolean): Promise { + if (isPanelPositionDown) { + return Promise.resolve(this.navigateToEditorGroup(GroupLocation.LAST)); + } + + return Promise.resolve(false); + } +} + +class NavigateDownAction extends BaseNavigationAction { + + static readonly ID = 'workbench.action.navigateDown'; + static readonly LABEL = nls.localize('navigateDown', "Navigate to the View Below"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IPanelService panelService: IPanelService, + @IPartService partService: IPartService, + @IViewletService viewletService: IViewletService + ) { + super(id, label, editorGroupService, panelService, partService, viewletService); + } + + protected navigateOnEditorFocus(_isSidebarPositionLeft: boolean, isPanelPositionDown: boolean): Promise { + const didNavigate = this.navigateAcrossEditorGroup(GroupDirection.DOWN); + if (didNavigate) { + return Promise.resolve(true); + } + + if (isPanelPositionDown) { + return Promise.resolve(this.navigateToPanel()); + } + + return Promise.resolve(false); + } +} + +const registry = Registry.as(Extensions.WorkbenchActions); +const viewCategory = nls.localize('view', "View"); + +registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateUpAction, NavigateUpAction.ID, NavigateUpAction.LABEL, undefined), 'View: Navigate to the View Above', viewCategory); +registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateDownAction, NavigateDownAction.ID, NavigateDownAction.LABEL, undefined), 'View: Navigate to the View Below', viewCategory); +registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateLeftAction, NavigateLeftAction.ID, NavigateLeftAction.LABEL, undefined), 'View: Navigate to the View on the Left', viewCategory); +registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateRightAction, NavigateRightAction.ID, NavigateRightAction.LABEL, undefined), 'View: Navigate to the View on the Right', viewCategory); diff --git a/src/vs/workbench/browser/actions/toggleActivityBarVisibility.ts b/src/vs/workbench/browser/actions/toggleActivityBarVisibility.ts deleted file mode 100644 index f28a090fa36..00000000000 --- a/src/vs/workbench/browser/actions/toggleActivityBarVisibility.ts +++ /dev/null @@ -1,50 +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 { Registry } from 'vs/platform/registry/common/platform'; -import { Action } from 'vs/base/common/actions'; -import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; -import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; - -export class ToggleActivityBarVisibilityAction extends Action { - - static readonly ID = 'workbench.action.toggleActivityBarVisibility'; - static readonly LABEL = nls.localize('toggleActivityBar', "Toggle Activity Bar Visibility"); - - private static readonly activityBarVisibleKey = 'workbench.activityBar.visible'; - - constructor( - id: string, - label: string, - @IPartService private readonly partService: IPartService, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); - - this.enabled = !!this.partService; - } - - run(): Promise { - const visibility = this.partService.isVisible(Parts.ACTIVITYBAR_PART); - const newVisibilityValue = !visibility; - - return this.configurationService.updateValue(ToggleActivityBarVisibilityAction.activityBarVisibleKey, newVisibilityValue, ConfigurationTarget.USER); - } -} - -const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleActivityBarVisibilityAction, ToggleActivityBarVisibilityAction.ID, ToggleActivityBarVisibilityAction.LABEL), 'View: Toggle Activity Bar Visibility', nls.localize('view', "View")); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: ToggleActivityBarVisibilityAction.ID, - title: nls.localize({ key: 'miToggleActivityBar', comment: ['&& denotes a mnemonic'] }, "Toggle &&Activity Bar") - }, - order: 4 -}); diff --git a/src/vs/workbench/browser/actions/toggleCenteredLayout.ts b/src/vs/workbench/browser/actions/toggleCenteredLayout.ts deleted file mode 100644 index 9e664ceec23..00000000000 --- a/src/vs/workbench/browser/actions/toggleCenteredLayout.ts +++ /dev/null @@ -1,44 +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 { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { IPartService } from 'vs/workbench/services/part/common/partService'; - -class ToggleCenteredLayout extends Action { - - static readonly ID = 'workbench.action.toggleCenteredLayout'; - static readonly LABEL = nls.localize('toggleCenteredLayout', "Toggle Centered Layout"); - - constructor( - id: string, - label: string, - @IPartService private readonly partService: IPartService - ) { - super(id, label); - this.enabled = !!this.partService; - } - - run(): Promise { - this.partService.centerEditorLayout(!this.partService.isEditorLayoutCentered()); - - return Promise.resolve(null); - } -} - -const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleCenteredLayout, ToggleCenteredLayout.ID, ToggleCenteredLayout.LABEL), 'View: Toggle Centered Layout', nls.localize('view', "View")); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '1_toggle_view', - command: { - id: ToggleCenteredLayout.ID, - title: nls.localize('miToggleCenteredLayout', "Toggle Centered Layout") - }, - order: 3 -}); diff --git a/src/vs/workbench/browser/actions/toggleEditorLayout.ts b/src/vs/workbench/browser/actions/toggleEditorLayout.ts deleted file mode 100644 index 3e31b9b481c..00000000000 --- a/src/vs/workbench/browser/actions/toggleEditorLayout.ts +++ /dev/null @@ -1,83 +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 'vs/css!./media/actions'; -import * as nls from 'vs/nls'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Action } from 'vs/base/common/actions'; -import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorGroupsService, GroupOrientation } from 'vs/workbench/services/group/common/editorGroupsService'; - -export class ToggleEditorLayoutAction extends Action { - - static readonly ID = 'workbench.action.toggleEditorGroupLayout'; - static readonly LABEL = nls.localize('flipLayout', "Toggle Vertical/Horizontal Editor Layout"); - - private toDispose: IDisposable[]; - - constructor( - id: string, - label: string, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService - ) { - super(id, label); - - this.toDispose = []; - - this.class = 'flip-editor-layout'; - this.updateEnablement(); - - this.registerListeners(); - } - - private registerListeners(): void { - this.toDispose.push(this.editorGroupService.onDidAddGroup(() => this.updateEnablement())); - this.toDispose.push(this.editorGroupService.onDidRemoveGroup(() => this.updateEnablement())); - } - - private updateEnablement(): void { - this.enabled = this.editorGroupService.count > 1; - } - - run(): Promise { - const newOrientation = (this.editorGroupService.orientation === GroupOrientation.VERTICAL) ? GroupOrientation.HORIZONTAL : GroupOrientation.VERTICAL; - this.editorGroupService.setGroupOrientation(newOrientation); - - return Promise.resolve(null); - } - - dispose(): void { - this.toDispose = dispose(this.toDispose); - - super.dispose(); - } -} - -CommandsRegistry.registerCommand('_workbench.editor.setGroupOrientation', function (accessor: ServicesAccessor, args: [GroupOrientation]) { - const editorGroupService = accessor.get(IEditorGroupsService); - const [orientation] = args; - - editorGroupService.setGroupOrientation(orientation); - - return Promise.resolve(null); -}); - -const registry = Registry.as(Extensions.WorkbenchActions); -const group = nls.localize('view', "View"); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleEditorLayoutAction, ToggleEditorLayoutAction.ID, ToggleEditorLayoutAction.LABEL, { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_0, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_0 } }), 'View: Flip Editor Group Layout', group); - -MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: 'z_flip', - command: { - id: ToggleEditorLayoutAction.ID, - title: nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Flip &&Layout") - }, - order: 1 -}); diff --git a/src/vs/workbench/browser/actions/toggleSidebarPosition.ts b/src/vs/workbench/browser/actions/toggleSidebarPosition.ts deleted file mode 100644 index 9eb8bd8ae8d..00000000000 --- a/src/vs/workbench/browser/actions/toggleSidebarPosition.ts +++ /dev/null @@ -1,54 +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 { Registry } from 'vs/platform/registry/common/platform'; -import { Action } from 'vs/base/common/actions'; -import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { IPartService, Position } from 'vs/workbench/services/part/common/partService'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; - -export class ToggleSidebarPositionAction extends Action { - - static readonly ID = 'workbench.action.toggleSidebarPosition'; - static readonly LABEL = nls.localize('toggleSidebarPosition', "Toggle Side Bar Position"); - - private static readonly sidebarPositionConfigurationKey = 'workbench.sideBar.location'; - - constructor( - id: string, - label: string, - @IPartService private readonly partService: IPartService, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); - - this.enabled = !!this.partService && !!this.configurationService; - } - - run(): Promise { - const position = this.partService.getSideBarPosition(); - const newPositionValue = (position === Position.LEFT) ? 'right' : 'left'; - - return this.configurationService.updateValue(ToggleSidebarPositionAction.sidebarPositionConfigurationKey, newPositionValue, ConfigurationTarget.USER); - } - - static getLabel(partService: IPartService): string { - return partService.getSideBarPosition() === Position.LEFT ? nls.localize('moveSidebarRight', "Move Side Bar Right") : nls.localize('moveSidebarLeft', "Move Side Bar Left"); - } -} - -const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.LABEL), 'View: Toggle Side Bar Position', nls.localize('view', "View")); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: ToggleSidebarPositionAction.ID, - title: nls.localize({ key: 'miMoveSidebarLeftRight', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Left/Right") - }, - order: 2 -}); diff --git a/src/vs/workbench/browser/actions/toggleSidebarVisibility.ts b/src/vs/workbench/browser/actions/toggleSidebarVisibility.ts deleted file mode 100644 index 48a2d0a95b8..00000000000 --- a/src/vs/workbench/browser/actions/toggleSidebarVisibility.ts +++ /dev/null @@ -1,47 +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 { Registry } from 'vs/platform/registry/common/platform'; -import { Action } from 'vs/base/common/actions'; -import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; -import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; - -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, - @IPartService private readonly partService: IPartService - ) { - super(id, label); - - this.enabled = !!this.partService; - } - - run(): Promise { - const hideSidebar = this.partService.isVisible(Parts.SIDEBAR_PART); - this.partService.setSideBarHidden(hideSidebar); - - return Promise.resolve(null); - } -} - -const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleSidebarVisibilityAction, ToggleSidebarVisibilityAction.ID, ToggleSidebarVisibilityAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_B }), 'View: Toggle Side Bar Visibility', nls.localize('view', "View")); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: ToggleSidebarVisibilityAction.ID, - title: nls.localize({ key: 'miToggleSidebar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Side Bar") - }, - order: 1 -}); diff --git a/src/vs/workbench/browser/actions/toggleStatusbarVisibility.ts b/src/vs/workbench/browser/actions/toggleStatusbarVisibility.ts deleted file mode 100644 index 277eff7fcf7..00000000000 --- a/src/vs/workbench/browser/actions/toggleStatusbarVisibility.ts +++ /dev/null @@ -1,50 +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 { Registry } from 'vs/platform/registry/common/platform'; -import { Action } from 'vs/base/common/actions'; -import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; - -export class ToggleStatusbarVisibilityAction extends Action { - - static readonly ID = 'workbench.action.toggleStatusbarVisibility'; - static readonly LABEL = nls.localize('toggleStatusbar', "Toggle Status Bar Visibility"); - - private static readonly statusbarVisibleKey = 'workbench.statusBar.visible'; - - constructor( - id: string, - label: string, - @IPartService private readonly partService: IPartService, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); - - this.enabled = !!this.partService; - } - - run(): Promise { - const visibility = this.partService.isVisible(Parts.STATUSBAR_PART); - const newVisibilityValue = !visibility; - - return this.configurationService.updateValue(ToggleStatusbarVisibilityAction.statusbarVisibleKey, newVisibilityValue, ConfigurationTarget.USER); - } -} - -const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleStatusbarVisibilityAction, ToggleStatusbarVisibilityAction.ID, ToggleStatusbarVisibilityAction.LABEL), 'View: Toggle Status Bar Visibility', nls.localize('view', "View")); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: ToggleStatusbarVisibilityAction.ID, - title: nls.localize({ key: 'miToggleStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Status Bar") - }, - order: 3 -}); diff --git a/src/vs/workbench/browser/actions/toggleTabsVisibility.ts b/src/vs/workbench/browser/actions/toggleTabsVisibility.ts deleted file mode 100644 index bde2adca0a6..00000000000 --- a/src/vs/workbench/browser/actions/toggleTabsVisibility.ts +++ /dev/null @@ -1,38 +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 { Registry } from 'vs/platform/registry/common/platform'; -import { Action } from 'vs/base/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; - -export class ToggleTabsVisibilityAction extends Action { - - static readonly ID = 'workbench.action.toggleTabsVisibility'; - static readonly LABEL = nls.localize('toggleTabs', "Toggle Tab Visibility"); - - private static readonly tabsVisibleKey = 'workbench.editor.showTabs'; - - constructor( - id: string, - label: string, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); - } - - run(): Promise { - const visibility = this.configurationService.getValue(ToggleTabsVisibilityAction.tabsVisibleKey); - const newVisibilityValue = !visibility; - - return this.configurationService.updateValue(ToggleTabsVisibilityAction.tabsVisibleKey, newVisibilityValue); - } -} - -const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleTabsVisibilityAction, ToggleTabsVisibilityAction.ID, ToggleTabsVisibilityAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_W }), 'View: Toggle Tab Visibility', nls.localize('view', "View")); \ No newline at end of file diff --git a/src/vs/workbench/browser/actions/toggleZenMode.ts b/src/vs/workbench/browser/actions/toggleZenMode.ts deleted file mode 100644 index 73ed632b037..00000000000 --- a/src/vs/workbench/browser/actions/toggleZenMode.ts +++ /dev/null @@ -1,45 +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 { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { IPartService } from 'vs/workbench/services/part/common/partService'; - -class ToggleZenMode extends Action { - - static readonly ID = 'workbench.action.toggleZenMode'; - static readonly LABEL = nls.localize('toggleZenMode', "Toggle Zen Mode"); - - constructor( - id: string, - label: string, - @IPartService private readonly partService: IPartService - ) { - super(id, label); - this.enabled = !!this.partService; - } - - run(): Promise { - this.partService.toggleZenMode(); - - return Promise.resolve(null); - } -} - -const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleZenMode, ToggleZenMode.ID, ToggleZenMode.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_Z) }), 'View: Toggle Zen Mode', nls.localize('view', "View")); - -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '1_toggle_view', - command: { - id: ToggleZenMode.ID, - title: nls.localize('miToggleZenMode', "Toggle Zen Mode") - }, - order: 2 -}); diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index a2b9b6e36cf..9cfdc373556 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -17,6 +17,7 @@ import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL, PICK_WORKSPACE_FOLDE import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export class OpenFileAction extends Action { @@ -142,15 +143,14 @@ export class SaveWorkspaceAsAction extends Action { run(): Promise { return this.getNewWorkspaceConfigPath().then((configPathUri): Promise | void => { if (configPathUri) { - const configPath = configPathUri.fsPath; switch (this.contextService.getWorkbenchState()) { case WorkbenchState.EMPTY: case WorkbenchState.FOLDER: const folders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri })); - return this.workspaceEditingService.createAndEnterWorkspace(folders, configPath); + return this.workspaceEditingService.createAndEnterWorkspace(folders, configPathUri); case WorkbenchState.WORKSPACE: - return this.workspaceEditingService.saveAndEnterWorkspace(configPath); + return this.workspaceEditingService.saveAndEnterWorkspace(configPathUri); } } }); @@ -184,6 +184,32 @@ export class OpenWorkspaceAction extends Action { } } +export class CloseWorkspaceAction extends Action { + + static readonly ID = 'workbench.action.closeFolder'; + static LABEL = nls.localize('closeWorkspace', "Close Workspace"); + + constructor( + id: string, + label: string, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @INotificationService private readonly notificationService: INotificationService, + @IWindowService private readonly windowService: IWindowService + ) { + super(id, label); + } + + 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.")); + + return Promise.resolve(undefined); + } + + return this.windowService.closeWorkspace(); + } +} + export class OpenWorkspaceConfigFileAction extends Action { static readonly ID = 'workbench.action.openWorkspaceConfigFile'; @@ -228,7 +254,7 @@ export class DuplicateWorkspaceInNewWindowAction extends Action { run(): Promise { const folders = this.workspaceContextService.getWorkspace().folders; - return this.workspacesService.createWorkspace(folders).then(newWorkspace => { + return this.workspacesService.createUntitledWorkspace(folders).then(newWorkspace => { return this.workspaceEditingService.copyWorkspaceSettings(newWorkspace).then(() => { return this.windowService.openWindow([URI.file(newWorkspace.configPath)], { forceNewWindow: true }); }); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index bde4319b2d3..e73acff052b 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -295,7 +295,7 @@ export class ResourcesDropHandler { // Multiple folders: Create new workspace with folders and open else if (folders.length > 1) { - workspacesToOpen = this.workspacesService.createWorkspace(folders.map(folder => ({ uri: folder }))).then(workspace => [URI.file(workspace.configPath)]); + workspacesToOpen = this.workspacesService.createUntitledWorkspace(folders.map(folder => ({ uri: folder }))).then(workspace => [URI.file(workspace.configPath)]); } // Open diff --git a/src/vs/workbench/browser/media/part.css b/src/vs/workbench/browser/media/part.css index fc87a52eaf3..32c3fe76eae 100644 --- a/src/vs/workbench/browser/media/part.css +++ b/src/vs/workbench/browser/media/part.css @@ -3,34 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench > .part > .title { +.monaco-workbench .part > .title { display: none; /* Parts have to opt in to show title area */ } -.monaco-workbench > .part > .title { +.monaco-workbench .part > .title { height: 35px; display: flex; box-sizing: border-box; overflow: hidden; } -.monaco-workbench > .part > .title { +.monaco-workbench .part > .title { padding-left: 8px; padding-right: 8px; } -.monaco-workbench > .part > .title > .title-label { +.monaco-workbench .part > .title > .title-label { line-height: 35px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.monaco-workbench > .part > .title > .title-label { +.monaco-workbench .part > .title > .title-label { padding-left: 12px; } -.monaco-workbench > .part > .title > .title-label h2 { +.monaco-workbench .part > .title > .title-label h2 { font-size: 11px; cursor: default; font-weight: normal; @@ -41,19 +41,19 @@ text-overflow: ellipsis; } -.monaco-workbench > .part > .title > .title-label a { +.monaco-workbench .part > .title > .title-label a { text-decoration: none; font-size: 13px; cursor: default; } -.monaco-workbench > .part > .title > .title-actions { +.monaco-workbench .part > .title > .title-actions { height: 35px; flex: 1; padding-left: 5px; } -.monaco-workbench > .part > .title > .title-actions .action-label { +.monaco-workbench .part > .title > .title-actions .action-label { display: block; height: 35px; line-height: 35px; @@ -63,16 +63,16 @@ background-repeat: no-repeat; } -.monaco-workbench > .part > .title > .title-actions .action-label .label { +.monaco-workbench .part > .title > .title-actions .action-label .label { display: none; } -.monaco-workbench > .part > .content { +.monaco-workbench .part > .content { font-size: 13px; } -.monaco-workbench > .part > .content > .monaco-progress-container, -.monaco-workbench > .part.editor > .content .monaco-progress-container { +.monaco-workbench .part > .content > .monaco-progress-container, +.monaco-workbench .part.editor > .content .monaco-progress-container { position: absolute; left: 0; top: 33px; /* at the bottom of the 35px height title container */ @@ -80,7 +80,7 @@ height: 2px; } -.monaco-workbench > .part > .content > .monaco-progress-container .progress-bit, -.monaco-workbench > .part.editor > .content .monaco-progress-container .progress-bit { +.monaco-workbench .part > .content > .monaco-progress-container .progress-bit, +.monaco-workbench .part.editor > .content .monaco-progress-container .progress-bit { height: 2px; } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 404646c23f5..1f55bd25236 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -6,6 +6,7 @@ import 'vs/css!./media/activitybarpart'; import * as nls from 'vs/nls'; import { illegalArgument } from 'vs/base/common/errors'; +import { Emitter } from 'vs/base/common/event'; import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { GlobalActivityExtensions, IGlobalActivityRegistry } from 'vs/workbench/common/activity'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -16,7 +17,7 @@ import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { IPartService, Parts, Position as SideBarPosition } from 'vs/workbench/services/part/common/partService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ToggleActivityBarVisibilityAction } from 'vs/workbench/browser/actions/toggleActivityBarVisibility'; +import { ToggleActivityBarVisibilityAction } from 'vs/workbench/browser/actions/layoutActions'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; @@ -31,6 +32,7 @@ import { IViewsService, IViewContainersRegistry, Extensions as ViewContainerExte import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { isUndefinedOrNull } from 'vs/base/common/types'; +import { ISerializableView } from 'vs/base/browser/ui/grid/grid'; const SCM_VIEWLET_ID = 'workbench.view.scm'; @@ -43,7 +45,7 @@ interface ICachedViewlet { views?: { when: string }[]; } -export class ActivitybarPart extends Part { +export class ActivitybarPart extends Part implements ISerializableView { private static readonly ACTION_HEIGHT = 50; private static readonly PINNED_VIEWLETS = 'workbench.activity.pinnedViewlets'; @@ -57,6 +59,15 @@ export class ActivitybarPart extends Part { private compositeBar: CompositeBar; private compositeActions: { [compositeId: string]: { activityAction: ViewletActivityAction, pinnedAction: ToggleCompositePinnedAction } } = Object.create(null); + element: HTMLElement; + minimumWidth: number = 50; + maximumWidth: number = 50; + minimumHeight: number = 0; + maximumHeight: number = Number.POSITIVE_INFINITY; + + private _onDidChange = new Emitter<{ width: number; height: number; }>(); + readonly onDidChange = this._onDidChange.event; + constructor( id: string, @IViewletService private readonly viewletService: IViewletService, @@ -180,6 +191,7 @@ export class ActivitybarPart extends Part { } createContentArea(parent: HTMLElement): HTMLElement { + this.element = parent; const content = document.createElement('div'); addClass(content, 'content'); parent.appendChild(content); @@ -338,13 +350,19 @@ export class ActivitybarPart extends Part { .map(v => v.id); } - layout(dimension: Dimension): Dimension[] { + layout(dimension: Dimension): Dimension[]; + layout(width: number, height: number): void; + layout(dim1: Dimension | number, dim2?: number): Dimension[] | void { if (!this.partService.isVisible(Parts.ACTIVITYBAR_PART)) { - return [dimension]; + if (dim1 instanceof Dimension) { + return [dim1]; + } + + return; } // Pass to super - const sizes = super.layout(dimension); + const sizes = super.layout(dim1 instanceof Dimension ? dim1 : new Dimension(dim1, dim2)); this.dimension = sizes[1]; @@ -353,9 +371,11 @@ export class ActivitybarPart extends Part { // adjust height for global actions showing availableHeight -= (this.globalActionBar.items.length * ActivitybarPart.ACTION_HEIGHT); } - this.compositeBar.layout(new Dimension(dimension.width, availableHeight)); + this.compositeBar.layout(new Dimension(dim1 instanceof Dimension ? dim1.width : dim1, availableHeight)); - return sizes; + if (dim1 instanceof Dimension) { + return sizes; + } } private onDidStorageChange(e: IWorkspaceStorageChangeEvent): void { @@ -469,4 +489,10 @@ export class ActivitybarPart extends Part { const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); return viewContainerRegistry.get(viewletId); } + + toJSON(): object { + return { + type: Parts.ACTIVITYBAR_PART + }; + } } diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index 3f07bfcb0d0..362ea4a70c8 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item { +.monaco-workbench .activitybar > .content .monaco-action-bar .action-item { display: block; position: relative; padding: 5px 0; } -.monaco-workbench > .activitybar > .content .monaco-action-bar .action-label { +.monaco-workbench .activitybar > .content .monaco-action-bar .action-label { display: flex; overflow: hidden; height: 40px; @@ -20,7 +20,7 @@ font-size: 15px; } -.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item:focus:before { +.monaco-workbench .activitybar > .content .monaco-action-bar .action-item:focus:before { content: ""; position: absolute; top: 9px; @@ -29,19 +29,19 @@ border-left: 2px solid; } -.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item.clicked:focus:before { +.monaco-workbench .activitybar > .content .monaco-action-bar .action-item.clicked:focus:before { border-left: none !important; /* no focus feedback when using mouse */ } -.monaco-workbench > .activitybar.left > .content .monaco-action-bar .action-item:focus:before { +.monaco-workbench .activitybar.left > .content .monaco-action-bar .action-item:focus:before { left: 1px; } -.monaco-workbench > .activitybar.right > .content .monaco-action-bar .action-item:focus:before { +.monaco-workbench .activitybar.right > .content .monaco-action-bar .action-item:focus:before { right: 1px; } -.monaco-workbench > .activitybar > .content .monaco-action-bar .badge { +.monaco-workbench .activitybar > .content .monaco-action-bar .badge { position: absolute; top: 5px; left: 0; @@ -50,7 +50,7 @@ height: 40px; } -.monaco-workbench > .activitybar > .content .monaco-action-bar .badge .badge-content { +.monaco-workbench .activitybar > .content .monaco-action-bar .badge .badge-content { position: absolute; top: 20px; right: 8px; @@ -65,13 +65,13 @@ /* Right aligned */ -.monaco-workbench > .activitybar.right > .content .monaco-action-bar .action-label { +.monaco-workbench .activitybar.right > .content .monaco-action-bar .action-label { margin-left: 0; padding: 0 50px 0 0; background-position: calc(100% - 9px) center; } -.monaco-workbench > .activitybar.right > .content .monaco-action-bar .badge { +.monaco-workbench .activitybar.right > .content .monaco-action-bar .badge { left: auto; right: 0; } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index 5a92b2e1f52..3b72852b0fc 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -3,23 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench > .part.activitybar { +.monaco-workbench .part.activitybar { width: 50px; } -.monaco-workbench > .activitybar > .content { +.monaco-workbench .activitybar > .content { height: 100%; display: flex; flex-direction: column; justify-content: space-between; } -.monaco-workbench > .activitybar > .content .monaco-action-bar { +.monaco-workbench .activitybar > .content .monaco-action-bar { text-align: left; background-color: inherit; } -.monaco-workbench > .activitybar .action-item:focus { +.monaco-workbench .activitybar .action-item:focus { outline: 0 !important; /* activity bar indicates focus custom */ } diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 1865d8b1d70..8064787e489 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -464,10 +464,11 @@ export abstract class CompositePart extends Part { return AnchorAlignment.RIGHT; } - layout(dimension: Dimension): Dimension[] { - + layout(dimension: Dimension): Dimension[]; + layout(width: number, height: number): void; + layout(dim1: Dimension | number, dim2?: number): Dimension[] | void { // Pass to super - const sizes = super.layout(dimension); + const sizes = super.layout(dim1 instanceof Dimension ? dim1 : new Dimension(dim1, dim2!)); // Pass Contentsize to composite this.contentAreaSize = sizes[1]; @@ -475,7 +476,9 @@ export abstract class CompositePart extends Part { this.activeComposite.layout(this.contentAreaSize); } - return sizes; + if (dim1 instanceof Dimension) { + return sizes; + } } protected removeComposite(compositeId: string): boolean { diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/baseEditor.ts index c0f00f4acf6..0e2df2423a3 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/baseEditor.ts @@ -118,6 +118,7 @@ export abstract class BaseEditor extends Panel implements IEditor { setVisible(visible: boolean, group?: IEditorGroup): void { super.setVisible(visible); + // Propagate to Editor this.setEditorVisible(visible, group); } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbs.ts b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts index 93f4fe6aeaa..43ae1ecc383 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbs.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts @@ -23,7 +23,7 @@ export interface IBreadcrumbsService { register(group: GroupIdentifier, widget: BreadcrumbsWidget): IDisposable; - getWidget(group: GroupIdentifier): BreadcrumbsWidget; + getWidget(group: GroupIdentifier): BreadcrumbsWidget | undefined; } @@ -43,7 +43,7 @@ export class BreadcrumbsService implements IBreadcrumbsService { }; } - getWidget(group: number): BreadcrumbsWidget { + getWidget(group: number): BreadcrumbsWidget | undefined { return this._map.get(group); } } @@ -90,10 +90,18 @@ export abstract class BreadcrumbsConfig { readonly name = name; readonly onDidChange = onDidChange.event; getValue(overrides?: IConfigurationOverrides): T { - return service.getValue(name, overrides); + if (overrides) { + return service.getValue(name, overrides); + } else { + return service.getValue(name); + } } updateValue(newValue: T, overrides?: IConfigurationOverrides): Promise { - return service.updateValue(name, newValue, overrides); + if (overrides) { + return service.updateValue(name, newValue, overrides); + } else { + return service.updateValue(name, newValue); + } } dispose(): void { listener.dispose(); diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts index 9ef5e5681fa..5edc694dd85 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -31,7 +31,7 @@ export class FileElement { export type BreadcrumbElement = FileElement | OutlineModel | OutlineGroup | OutlineElement; -type FileInfo = { path: FileElement[], folder: IWorkspaceFolder }; +type FileInfo = { path: FileElement[], folder?: IWorkspaceFolder }; export class EditorBreadcrumbsModel { @@ -105,17 +105,19 @@ export class EditorBreadcrumbsModel { } let info: FileInfo = { - folder: workspaceService.getWorkspaceFolder(uri), + folder: workspaceService.getWorkspaceFolder(uri) || undefined, path: [] }; - while (uri.path !== '/') { - if (info.folder && isEqual(info.folder.uri, uri)) { + + let uriPrefix: URI | null = uri; + while (uriPrefix && uriPrefix.path !== '/') { + if (info.folder && isEqual(info.folder.uri, uriPrefix)) { break; } - info.path.unshift(new FileElement(uri, info.path.length === 0 ? FileKind.FILE : FileKind.FOLDER)); - let prevPathLength = uri.path.length; - uri = dirname(uri); - if (uri.path.length === prevPathLength) { + info.path.unshift(new FileElement(uriPrefix, info.path.length === 0 ? FileKind.FILE : FileKind.FOLDER)); + let prevPathLength = uriPrefix.path.length; + uriPrefix = dirname(uriPrefix); + if (!uriPrefix || uriPrefix.path.length === prevPathLength) { break; } } @@ -148,7 +150,9 @@ export class EditorBreadcrumbsModel { this._updateOutlineElements([]); } - const buffer = this._editor.getModel(); + const editor = this._editor!; + + const buffer = editor.getModel(); if (!buffer || !DocumentSymbolProviderRegistry.has(buffer) || !isEqual(buffer.uri, this._uri)) { return; } @@ -174,11 +178,11 @@ export class EditorBreadcrumbsModel { // copy the model model = model.adopt(); - this._updateOutlineElements(this._getOutlineElements(model, this._editor.getPosition())); - this._outlineDisposables.push(this._editor.onDidChangeCursorPosition(_ => { + this._updateOutlineElements(this._getOutlineElements(model, editor.getPosition())); + this._outlineDisposables.push(editor.onDidChangeCursorPosition(_ => { timeout.cancelAndSet(() => { - if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId() && this._editor.getModel()) { - this._updateOutlineElements(this._getOutlineElements(model, this._editor.getPosition())); + if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId() && editor.getModel()) { + this._updateOutlineElements(this._getOutlineElements(model, editor.getPosition())); } }, 150); })); @@ -189,11 +193,11 @@ export class EditorBreadcrumbsModel { }); } - private _getOutlineElements(model: OutlineModel, position: IPosition): Array { - if (!model) { + private _getOutlineElements(model: OutlineModel, position: IPosition | null): Array { + if (!model || !position) { return []; } - let item: OutlineGroup | OutlineElement = model.getItemEnclosingPosition(position); + let item: OutlineGroup | OutlineElement | undefined = model.getItemEnclosingPosition(position); if (!item) { return [model]; } @@ -204,7 +208,7 @@ export class EditorBreadcrumbsModel { if (parent instanceof OutlineModel) { break; } - if (parent instanceof OutlineGroup && size(parent.parent.children) === 1) { + if (parent instanceof OutlineGroup && parent.parent && size(parent.parent.children) === 1) { break; } item = parent; diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 6d34336e1ab..b262c8aeef8 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -50,6 +50,7 @@ import { AllEditorsPicker, ActiveEditorGroupPicker } from 'vs/workbench/browser/ import { Schemas } from 'vs/base/common/network'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { OpenWorkspaceButtonContribution } from 'vs/workbench/browser/parts/editor/editorWidgets'; +import { ZoomStatusbarItem } from 'vs/workbench/browser/parts/editor/resourceViewer'; // Register String Editor Registry.as(EditorExtensions.Editors).registerEditor( @@ -221,6 +222,9 @@ registerEditorContribution(OpenWorkspaceButtonContribution); const statusBar = Registry.as(StatusExtensions.Statusbar); statusBar.registerStatusbarItem(new StatusbarItemDescriptor(EditorStatus, StatusbarAlignment.RIGHT, 100 /* towards the left of the right hand side */)); +// Register Zoom Status +statusBar.registerStatusbarItem(new StatusbarItemDescriptor(ZoomStatusbarItem, StatusbarAlignment.RIGHT, 101 /* to the left of editor status (100) */)); + // Register Status Actions const registry = Registry.as(ActionExtensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(ChangeModeAction, ChangeModeAction.ID, ChangeModeAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_M) }), 'Change Language Mode'); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 9bb84cf7d49..3a5fdebc6a3 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -1460,7 +1460,7 @@ registerThemingParticipant((theme, collector, environment) => { // Letterpress const letterpress = `resources/letterpress${theme.type === 'dark' ? '-dark' : theme.type === 'hc' ? '-hc' : ''}.svg`; collector.addRule(` - .monaco-workbench > .part.editor > .content .editor-group-container.empty .editor-group-letterpress { + .monaco-workbench .part.editor > .content .editor-group-container.empty .editor-group-letterpress { background-image: url('${URI.file(join(environment.appRoot, letterpress)).toString()}') } `); @@ -1469,20 +1469,20 @@ registerThemingParticipant((theme, collector, environment) => { const focusedEmptyGroupBorder = theme.getColor(EDITOR_GROUP_FOCUSED_EMPTY_BORDER); if (focusedEmptyGroupBorder) { collector.addRule(` - .monaco-workbench > .part.editor > .content:not(.empty) .editor-group-container.empty.active:focus { + .monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty.active:focus { outline-width: 1px; outline-color: ${focusedEmptyGroupBorder}; outline-offset: -2px; outline-style: solid; } - .monaco-workbench > .part.editor > .content.empty .editor-group-container.empty.active:focus { + .monaco-workbench .part.editor > .content.empty .editor-group-container.empty.active:focus { outline: none; /* never show outline for empty group if it is the last */ } `); } else { collector.addRule(` - .monaco-workbench > .part.editor > .content .editor-group-container.empty.active:focus { + .monaco-workbench .part.editor > .content .editor-group-container.empty.active:focus { outline: none; /* disable focus outline unless active empty group border is defined */ } `); diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index adfe1690863..d64cb6ecce4 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -11,7 +11,7 @@ import { Event, Emitter, Relay } from 'vs/base/common/event'; import { contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { GroupDirection, IAddGroupOptions, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, ICopyEditorOptions, GroupsOrder, GroupChangeKind, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument } from 'vs/workbench/services/group/common/editorGroupsService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Direction, SerializableGrid, Sizing, ISerializedGrid, Orientation, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid, Grid } from 'vs/base/browser/ui/grid/grid'; +import { Direction, SerializableGrid, Sizing, ISerializedGrid, Orientation, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid, Grid, ISerializableView } from 'vs/base/browser/ui/grid/grid'; import { GroupIdentifier, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { values } from 'vs/base/common/map'; import { EDITOR_GROUP_BORDER, EDITOR_PANE_BACKGROUND } from 'vs/workbench/common/theme'; @@ -23,13 +23,13 @@ import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { assign } from 'vs/base/common/objects'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup'; -import { always } from 'vs/base/common/async'; import { EditorDropTarget } from 'vs/workbench/browser/parts/editor/editorDropTarget'; import { localize } from 'vs/nls'; import { Color } from 'vs/base/common/color'; import { CenteredViewLayout } from 'vs/base/browser/ui/centered/centeredViewLayout'; -import { IView, orthogonal } from 'vs/base/browser/ui/grid/gridview'; +import { IView, orthogonal, LayoutPriority } from 'vs/base/browser/ui/grid/gridview'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { Parts } from 'vs/workbench/services/part/common/partService'; interface IEditorPartUIState { serializedGrid: ISerializedGrid; @@ -79,7 +79,7 @@ class GridWidgetView implements IView { } } -export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditorGroupsAccessor { +export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditorGroupsAccessor, ISerializableView { _serviceBrand: any; @@ -132,6 +132,13 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor private _whenRestored: Promise; private whenRestoredResolve: () => void; + element: HTMLElement; + + private _onDidChange = new Emitter<{ width: number; height: number; }>(); + readonly onDidChange = this._onDidChange.event; + + priority: LayoutPriority = LayoutPriority.High; + constructor( id: string, private restorePreviousState: boolean, @@ -779,6 +786,7 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor createContentArea(parent: HTMLElement): HTMLElement { // Container + this.element = parent; this.container = document.createElement('div'); addClass(this.container, 'content'); parent.appendChild(this.container); @@ -820,7 +828,7 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor } // Signal restored - always(Promise.all(this.groups.map(group => group.whenRestored)), () => this.whenRestoredResolve()); + Promise.all(this.groups.map(group => group.whenRestored)).finally(() => this.whenRestoredResolve()); // Update container this.updateContainer(); @@ -946,12 +954,16 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor return this.groupViews.size === 1 && this._activeGroup.isEmpty(); } - layout(dimension: Dimension): Dimension[] { - const sizes = super.layout(dimension); + layout(dimension: Dimension): Dimension[]; + layout(width: number, height: number): void; + layout(dim1: Dimension | number, dim2?: number): Dimension[] | void { + const sizes = super.layout(dim1 instanceof Dimension ? dim1 : new Dimension(dim1, dim2)); this.doLayout(sizes[1]); - return sizes; + if (dim1 instanceof Dimension) { + return sizes; + } } private doLayout(dimension: Dimension): void { @@ -1007,4 +1019,10 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor } //#endregion + + toJSON(): object { + return { + type: Parts.EDITOR_PART + }; + } } diff --git a/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css b/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css index 2a5ce2936fe..d3850e29307 100644 --- a/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench>.part.editor>.content .editor-group-container .breadcrumbs-control.hidden { +.monaco-workbench .part.editor>.content .editor-group-container .breadcrumbs-control.hidden { display: none; } -.monaco-workbench>.part.editor>.content .editor-group-container .breadcrumbs-control .monaco-breadcrumb-item.selected .monaco-icon-label, -.monaco-workbench>.part.editor>.content .editor-group-container .breadcrumbs-control .monaco-breadcrumb-item.focused .monaco-icon-label { +.monaco-workbench .part.editor>.content .editor-group-container .breadcrumbs-control .monaco-breadcrumb-item.selected .monaco-icon-label, +.monaco-workbench .part.editor>.content .editor-group-container .breadcrumbs-control .monaco-breadcrumb-item.focused .monaco-icon-label { text-decoration-line: underline; } -.monaco-workbench>.part.editor>.content .editor-group-container .breadcrumbs-control .monaco-breadcrumb-item.selected .hint-more, -.monaco-workbench>.part.editor>.content .editor-group-container .breadcrumbs-control .monaco-breadcrumb-item.focused .hint-more { +.monaco-workbench .part.editor>.content .editor-group-container .breadcrumbs-control .monaco-breadcrumb-item.selected .hint-more, +.monaco-workbench .part.editor>.content .editor-group-container .breadcrumbs-control .monaco-breadcrumb-item.focused .hint-more { text-decoration-line: underline; } diff --git a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css index bd0c879f1c1..f372df090e5 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css +++ b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css @@ -5,26 +5,26 @@ /* Container */ -.monaco-workbench > .part.editor > .content .editor-group-container { +.monaco-workbench .part.editor > .content .editor-group-container { height: 100%; } -.monaco-workbench > .part.editor > .content .editor-group-container.empty { +.monaco-workbench .part.editor > .content .editor-group-container.empty { opacity: 0.5; /* dimmed to indicate inactive state */ } -.monaco-workbench > .part.editor > .content .editor-group-container.empty.active, -.monaco-workbench > .part.editor > .content .editor-group-container.empty.dragged-over { +.monaco-workbench .part.editor > .content .editor-group-container.empty.active, +.monaco-workbench .part.editor > .content .editor-group-container.empty.dragged-over { opacity: 1; /* indicate active/dragged-over group through undimmed state */ } /* Letterpress */ -.monaco-workbench > .part.editor > .content .editor-group-container > .editor-group-letterpress { +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-letterpress { display: none; /* only visible when empty */ } -.monaco-workbench > .part.editor > .content .editor-group-container.empty > .editor-group-letterpress { +.monaco-workbench .part.editor > .content .editor-group-container.empty > .editor-group-letterpress { display: block; margin: auto; width: 100%; @@ -35,25 +35,25 @@ background-size: 70% 70%; } -.monaco-workbench > .part.editor > .content.empty .editor-group-container.empty > .editor-group-letterpress { +.monaco-workbench .part.editor > .content.empty .editor-group-container.empty > .editor-group-letterpress { background-size: 100% 100%; /* larger for empty editor part */ height: 100%; /* no toolbar in this case */ } /* Title */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title { +.monaco-workbench .part.editor > .content .editor-group-container > .title { position: relative; box-sizing: border-box; overflow: hidden; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title:not(.tabs) { +.monaco-workbench .part.editor > .content .editor-group-container > .title:not(.tabs) { display: flex; /* when tabs are not shown, use flex layout */ flex-wrap: nowrap; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title.title-border-bottom::after { +.monaco-workbench .part.editor > .content .editor-group-container > .title.title-border-bottom::after { content: ''; position: absolute; bottom: 0; @@ -65,21 +65,21 @@ height: 1px; } -.monaco-workbench > .part.editor > .content .editor-group-container.empty > .title { +.monaco-workbench .part.editor > .content .editor-group-container.empty > .title { display: none; } /* Toolbar */ -.monaco-workbench > .part.editor > .content .editor-group-container > .editor-group-container-toolbar { +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-container-toolbar { display: none; } -.monaco-workbench > .part.editor > .content:not(.empty) .editor-group-container.empty > .editor-group-container-toolbar { +.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty > .editor-group-container-toolbar { display: block; } -.monaco-workbench > .part.editor > .content .editor-group-container > .editor-group-container-toolbar .action-label { +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-container-toolbar .action-label { display: block; height: 35px; line-height: 35px; @@ -89,26 +89,26 @@ background-repeat: no-repeat; } -.vs .monaco-workbench > .part.editor > .content .editor-group-container > .editor-group-container-toolbar .close-editor-group { +.vs .monaco-workbench .part.editor > .content .editor-group-container > .editor-group-container-toolbar .close-editor-group { background-image: url('close-big.svg'); } -.vs-dark .monaco-workbench > .part.editor > .content .editor-group-container > .editor-group-container-toolbar .close-editor-group, -.hc-black .monaco-workbench > .part.editor > .content .editor-group-container > .editor-group-container-toolbar .close-editor-group { +.vs-dark .monaco-workbench .part.editor > .content .editor-group-container > .editor-group-container-toolbar .close-editor-group, +.hc-black .monaco-workbench .part.editor > .content .editor-group-container > .editor-group-container-toolbar .close-editor-group { background-image: url('close-big-inverse.svg'); } /* Editor */ -.monaco-workbench > .part.editor > .content .editor-group-container.empty > .editor-container { +.monaco-workbench .part.editor > .content .editor-group-container.empty > .editor-container { display: none; } -.monaco-workbench > .part.editor > .content .editor-group-container > .editor-container > .editor-instance { +.monaco-workbench .part.editor > .content .editor-group-container > .editor-container > .editor-instance { height: 100%; } -.monaco-workbench > .part.editor > .content .grid-view-container { +.monaco-workbench .part.editor > .content .grid-view-container { width: 100%; height: 100%; } diff --git a/src/vs/workbench/browser/parts/editor/media/editorstatus.css b/src/vs/workbench/browser/parts/editor/media/editorstatus.css index 204d01c7f16..6770963641e 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorstatus.css +++ b/src/vs/workbench/browser/parts/editor/media/editorstatus.css @@ -22,7 +22,7 @@ cursor: default !important; } -.monaco-shell .screen-reader-detected-explanation { +.monaco-workbench .screen-reader-detected-explanation { width: 420px; top: 30px; right: 6px; @@ -30,7 +30,7 @@ cursor: default; } -.monaco-shell .screen-reader-detected-explanation .cancel { +.monaco-workbench .screen-reader-detected-explanation .cancel { position: absolute; top: 0; right: 0; @@ -41,27 +41,27 @@ cursor: pointer; } -.monaco-shell .screen-reader-detected-explanation h2 { +.monaco-workbench .screen-reader-detected-explanation h2 { margin: 0; padding: 0; font-weight: 400; font-size: 1.8em; } -.monaco-shell .screen-reader-detected-explanation p { +.monaco-workbench .screen-reader-detected-explanation p { font-size: 1.2em; } -.monaco-shell .screen-reader-detected-explanation hr { +.monaco-workbench .screen-reader-detected-explanation hr { border: 0; height: 2px; } -.monaco-shell .screen-reader-detected-explanation .buttons { +.monaco-workbench .screen-reader-detected-explanation .buttons { display: flex; } -.monaco-shell .screen-reader-detected-explanation .buttons a { +.monaco-workbench .screen-reader-detected-explanation .buttons a { font-size: 13px; padding-left: 12px; padding-right: 12px; @@ -69,11 +69,11 @@ max-width: fit-content; } -.monaco-shell.vs .screen-reader-detected-explanation .cancel { +.vs .monaco-workbench .screen-reader-detected-explanation .cancel { background: url('close-statusview.svg') center center no-repeat; } -.monaco-shell.vs-dark .screen-reader-detected-explanation .cancel, -.monaco-shell.hc-black .screen-reader-detected-explanation .cancel { +.vs-dark .monaco-workbench .screen-reader-detected-explanation .cancel, +.hc-black .monaco-workbench .screen-reader-detected-explanation .cancel { background: url('close-statusview-inverse.svg') center center no-repeat; } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css index 28760657fba..4c96ebbc1f1 100644 --- a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css @@ -5,7 +5,7 @@ /* Title Label */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title > .label-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title > .label-container { height: 35px; display: flex; justify-content: flex-start; @@ -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 .title-label { line-height: 35px; overflow: hidden; text-overflow: ellipsis; @@ -22,31 +22,31 @@ padding-left: 20px; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .no-tabs.title-label { +.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 { +.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 */ } /* Breadcrumbs */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control { +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control { flex: 1 50%; overflow: hidden; margin-left: .45em; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item { +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item { font-size: 0.9em; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control.preview .monaco-breadcrumb-item { +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control.preview .monaco-breadcrumb-item { font-style: italic; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before { +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before { content: '/'; opacity: 1; height: inherit; @@ -58,31 +58,33 @@ content: '\\'; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item.root_folder::before, -.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 .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item.root_folder::before, +.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, +.windows > .monaco-workbench .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; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item.root_folder::after { +.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'; - padding: 0px; + padding: 0; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child { +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child { padding-right: 4px; /* does not have trailing separator*/ } /* Title Actions */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .title-actions { +.monaco-workbench .part.editor > .content .editor-group-container > .title .title-actions { display: flex; flex: initial; opacity: 0.5; 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 aa867509495..1e413fdae98 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -5,37 +5,37 @@ /* 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 > .tabs-and-actions-container { display: flex; } -.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 > .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 { +.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 */ cursor: default; } /* Tabs Container */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container { display: flex; height: 35px; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container.scroll { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.scroll { overflow: scroll !important; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container::-webkit-scrollbar { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container::-webkit-scrollbar { display: none; } /* Tab */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab { position: relative; display: flex; white-space: nowrap; @@ -45,45 +45,45 @@ padding-left: 10px; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon-theme.close-button-right, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon-theme.close-button-off { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon-theme.close-button-right, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon-theme.close-button-off { padding-left: 5px; /* reduce padding when we show icons and are in shrinking mode and tab close button is not left */ } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit { width: 120px; min-width: fit-content; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink { min-width: 60px; flex-basis: 0; /* all tabs are even */ flex-grow: 1; /* all tabs grow even */ max-width: fit-content; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-button-left::after, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-button-off::after { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-button-left::after, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-button-off::after { 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) */ } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-button-left { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-button-left { min-width: 80px; /* make more room for close button when it shows to the left */ padding-right: 5px; /* we need less room when sizing is shrink */ } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dragged { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dragged { will-change: transform; /* forces tab to be drawn on a separate layer (fixes https://github.com/Microsoft/vscode/issues/18733) */ } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dragged-over div { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dragged-over div { pointer-events: none; /* prevents cursor flickering (fixes https://github.com/Microsoft/vscode/issues/38753) */ } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-left { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-left { flex-direction: row-reverse; padding-left: 0; padding-right: 10px; @@ -91,14 +91,14 @@ /* Tab border top/bottom */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-border-top-container, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-border-bottom-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-border-top-container, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-border-bottom-container { display: none; /* hidden by default until a color is provided (see below) */ } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container { display: block; position: absolute; left: 0; @@ -107,19 +107,19 @@ width: 100%; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container { 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 { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container { 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 { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container { top: 0; height: 2px; background-color: var(--tab-dirty-border-top-color); @@ -127,16 +127,16 @@ /* Tab Label */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label { margin-top: auto; margin-bottom: auto; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink .tab-label { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink .tab-label { position: relative; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .tab-label::after { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .tab-label::after { content: ''; position: absolute; right: 0; @@ -146,66 +146,66 @@ padding: 0; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink:focus > .tab-label::after { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink:focus > .tab-label::after { opacity: 0; /* when tab has the focus this shade breaks the tab border (fixes https://github.com/Microsoft/vscode/issues/57819) */ } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit .monaco-icon-label, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit .monaco-icon-label > .monaco-icon-label-description-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit .monaco-icon-label, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit .monaco-icon-label > .monaco-icon-label-description-container { overflow: visible; /* fixes https://github.com/Microsoft/vscode/issues/20182 */ } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .monaco-icon-label > .monaco-icon-label-description-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .monaco-icon-label > .monaco-icon-label-description-container { text-overflow: clip; } -.hc-black .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .monaco-icon-label > .monaco-icon-label-description-container { +.hc-black .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .monaco-icon-label > .monaco-icon-label-description-container { text-overflow: ellipsis; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab .monaco-icon-label::before { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab .monaco-icon-label::before { height: 16px; /* tweak the icon size of the editor labels when icons are enabled */ } /* Tab Close */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-close { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-close { margin-top: auto; margin-bottom: auto; width: 28px; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-right.sizing-shrink > .tab-close { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-right.sizing-shrink > .tab-close { flex: 0; overflow: hidden; /* let the close button 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.close-button-right.sizing-shrink > .tab-close, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-right.sizing-shrink:hover > .tab-close, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-right.sizing-shrink > .tab-close:focus-within { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.close-button-right.sizing-shrink > .tab-close, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-right.sizing-shrink:hover > .tab-close, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-right.sizing-shrink > .tab-close:focus-within { overflow: visible; /* ...but still show the close button on hover, focus and when dirty */ } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off > .tab-close { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off > .tab-close { display: none; /* hide the close action bar when we are configured to hide it */ } -.monaco-workbench > .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active > .tab-close .action-label, /* always show it for active tab */ -.monaco-workbench > .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab > .tab-close .action-label:focus, /* always show it on focus */ -.monaco-workbench > .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover > .tab-close .action-label, /* always show it on hover */ -.monaco-workbench > .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:hover > .tab-close .action-label, /* always show it on hover */ -.monaco-workbench > .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.dirty > .tab-close .action-label { /* always show it for dirty tabs */ +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active > .tab-close .action-label, /* always show it for active tab */ +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab > .tab-close .action-label:focus, /* always show it on focus */ +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover > .tab-close .action-label, /* always show it on hover */ +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:hover > .tab-close .action-label, /* always show it on hover */ +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.dirty > .tab-close .action-label { /* always show it for dirty tabs */ opacity: 1; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active > .tab-close .action-label, /* show dimmed for inactive group */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active:hover > .tab-close .action-label, /* show dimmed for inactive group */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-close .action-label, /* show dimmed for inactive group */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover > .tab-close .action-label { /* show dimmed for inactive group */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active > .tab-close .action-label, /* show dimmed for inactive group */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active:hover > .tab-close .action-label, /* show dimmed for inactive group */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-close .action-label, /* show dimmed for inactive group */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover > .tab-close .action-label { /* show dimmed for inactive group */ opacity: 0.5; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-close .action-label { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-close .action-label { opacity: 0; display: block; height: 16px; @@ -216,53 +216,53 @@ margin-right: 0.5em; } -.vs .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty .close-editor-action { +.vs .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty .close-editor-action { background: url('close-dirty.svg') center center no-repeat; } -.vs-dark .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty .close-editor-action, -.hc-black .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty .close-editor-action { +.vs-dark .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty .close-editor-action, +.hc-black .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty .close-editor-action { background: url('close-dirty-inverse.svg') center center no-repeat; } -.vs .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty .close-editor-action:hover { +.vs .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty .close-editor-action:hover { background: url('close.svg') center center no-repeat; } -.vs-dark .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty .close-editor-action:hover, -.hc-black .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty .close-editor-action:hover { +.vs-dark .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty .close-editor-action:hover, +.hc-black .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty .close-editor-action:hover { background: url('close-inverse.svg') center center no-repeat; } /* No Tab Close Button */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off { padding-right: 10px; /* give a little bit more room if close button is off */ } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-button-off { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-button-off { padding-right: 5px; /* we need less room when sizing is shrink */ } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty:not(.dirty-border-top) { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty:not(.dirty-border-top) { background-repeat: no-repeat; background-position-y: center; background-position-x: calc(100% - 6px); /* to the right of the tab label */ padding-right: 28px; /* make room for dirty indication when we are running without close button */ } -.vs .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty:not(.dirty-border-top) { +.vs .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty:not(.dirty-border-top) { background-image: url('close-dirty.svg'); } -.vs-dark .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty:not(.dirty-border-top), -.hc-black .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty { +.vs-dark .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty:not(.dirty-border-top), +.hc-black .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty { background-image: url('close-dirty-inverse.svg'); } /* Editor Actions */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .editor-actions { +.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions { cursor: default; flex: initial; padding-left: 4px; @@ -271,30 +271,30 @@ /* Breadcrumbs */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control { flex: 1 100%; height: 22px; cursor: default; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-icon-label { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-icon-label { height: 22px; line-height: 22px; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-icon-label::before { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-icon-label::before { 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 .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 { min-width: 16px; height: 22px; } -.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; } diff --git a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css index 41e78a153f2..42358c52102 100644 --- a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css @@ -5,31 +5,31 @@ /* Editor Label */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .title-label, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label { +.monaco-workbench .part.editor > .content .editor-group-container > .title .title-label, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label { white-space: nowrap; flex: 1; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .title-label a, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label a { +.monaco-workbench .part.editor > .content .editor-group-container > .title .title-label a, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label a { text-decoration: none; font-size: 13px; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .monaco-icon-label::before, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab .monaco-icon-label::before, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .title-label a, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label a, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .title-label h2, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label span { +.monaco-workbench .part.editor > .content .editor-group-container > .title .monaco-icon-label::before, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab .monaco-icon-label::before, +.monaco-workbench .part.editor > .content .editor-group-container > .title .title-label a, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label a, +.monaco-workbench .part.editor > .content .editor-group-container > .title .title-label h2, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label span { cursor: pointer; } /* Title Actions */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .title-actions .action-label, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .editor-actions .action-label { +.monaco-workbench .part.editor > .content .editor-group-container > .title .title-actions .action-label, +.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions .action-label { display: block; height: 35px; line-height: 35px; @@ -39,29 +39,29 @@ background-repeat: no-repeat; } -.hc-black .monaco-workbench > .part.editor > .content .editor-group-container > .title .title-actions .action-label, -.hc-black .monaco-workbench > .part.editor > .content .editor-group-container > .title .editor-actions .action-label { +.hc-black .monaco-workbench .part.editor > .content .editor-group-container > .title .title-actions .action-label, +.hc-black .monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions .action-label { line-height: initial; } -.monaco-workbench > .part.editor > .content .editor-group-container > .title .editor-actions .action-label .label, -.monaco-workbench > .part.editor > .content .editor-group-container > .title .title-actions .action-label .label { +.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions .action-label .label, +.monaco-workbench .part.editor > .content .editor-group-container > .title .title-actions .action-label .label { display: none; } /* Drag Cursor */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title { +.monaco-workbench .part.editor > .content .editor-group-container > .title { cursor: -webkit-grab; } /* Actions */ -.monaco-workbench > .part.editor > .content .editor-group-container > .title .close-editor-action { +.monaco-workbench .part.editor > .content .editor-group-container > .title .close-editor-action { background: url('close.svg') center center no-repeat; } -.vs-dark .monaco-workbench > .part.editor > .content .editor-group-container > .title .close-editor-action, -.hc-black .monaco-workbench > .part.editor > .content .editor-group-container > .title .close-editor-action { +.vs-dark .monaco-workbench .part.editor > .content .editor-group-container > .title .close-editor-action, +.hc-black .monaco-workbench .part.editor > .content .editor-group-container > .title .close-editor-action { background: url('close-inverse.svg') center center no-repeat; } diff --git a/src/vs/workbench/browser/parts/editor/resourceViewer.ts b/src/vs/workbench/browser/parts/editor/resourceViewer.ts index d819d0f555c..a0cc90b5735 100644 --- a/src/vs/workbench/browser/parts/editor/resourceViewer.ts +++ b/src/vs/workbench/browser/parts/editor/resourceViewer.ts @@ -13,12 +13,10 @@ import { LRUCache } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { clamp } from 'vs/base/common/numbers'; import { Themable } from 'vs/workbench/common/theme'; -import { IStatusbarItem, StatusbarItemDescriptor, IStatusbarRegistry, Extensions } from 'vs/workbench/browser/parts/statusbar/statusbar'; -import { StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; +import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IDisposable, Disposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { Registry } from 'vs/platform/registry/common/platform'; import { Action } from 'vs/base/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { memoize } from 'vs/base/common/decorators'; @@ -234,7 +232,7 @@ class FileSeemsBinaryFileView { type Scale = number | 'fit'; -class ZoomStatusbarItem extends Themable implements IStatusbarItem { +export class ZoomStatusbarItem extends Themable implements IStatusbarItem { static instance: ZoomStatusbarItem; @@ -314,10 +312,6 @@ class ZoomStatusbarItem extends Themable implements IStatusbarItem { } } -Registry.as(Extensions.Statusbar).registerStatusbarItem( - new StatusbarItemDescriptor(ZoomStatusbarItem, StatusbarAlignment.RIGHT, 101 /* to the left of editor status (100) */) -); - interface ImageState { scale: Scale; offsetX: number; diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index 6a7316293d8..9db3583b18d 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -176,17 +176,18 @@ export class SideBySideEditor extends BaseEditor { } private setNewInput(newInput: SideBySideEditorInput, options: EditorOptions, token: CancellationToken): Promise { - const detailsEditor = this._createEditor(newInput.details, this.detailsEditorContainer); - const masterEditor = this._createEditor(newInput.master, this.masterEditorContainer); + const detailsEditor = this.doCreateEditor(newInput.details, this.detailsEditorContainer); + const masterEditor = this.doCreateEditor(newInput.master, this.masterEditorContainer); return this.onEditorsCreated(detailsEditor, masterEditor, newInput.details, newInput.master, options, token); } - private _createEditor(editorInput: EditorInput, container: HTMLElement): BaseEditor { + private doCreateEditor(editorInput: EditorInput, container: HTMLElement): BaseEditor { const descriptor = Registry.as(EditorExtensions.Editors).getEditor(editorInput); if (!descriptor) { throw new Error('No descriptor for editor found'); } + const editor = descriptor.instantiate(this.instantiationService); editor.create(container); editor.setVisible(this.isVisible(), this.group); diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 3f531ff7b4d..5f957c221a6 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -1128,21 +1128,21 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const activeContrastBorderColor = theme.getColor(activeContrastBorder); if (activeContrastBorderColor) { collector.addRule(` - .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active, - .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active:hover { + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active, + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active:hover { outline: 1px solid; outline-offset: -5px; } - .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover { outline: 1px dashed; outline-offset: -5px; } - .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active > .tab-close .action-label, - .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active:hover > .tab-close .action-label, - .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-close .action-label, - .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover > .tab-close .action-label { + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active > .tab-close .action-label, + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active:hover > .tab-close .action-label, + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-close .action-label, + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover > .tab-close .action-label { opacity: 1 !important; } `); @@ -1152,7 +1152,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const tabHoverBackground = theme.getColor(TAB_HOVER_BACKGROUND); if (tabHoverBackground) { collector.addRule(` - .monaco-workbench > .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover { background-color: ${tabHoverBackground} !important; } `); @@ -1161,7 +1161,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const tabUnfocusedHoverBackground = theme.getColor(TAB_UNFOCUSED_HOVER_BACKGROUND); if (tabUnfocusedHoverBackground) { collector.addRule(` - .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover { background-color: ${tabUnfocusedHoverBackground} !important; } `); @@ -1171,7 +1171,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const tabHoverBorder = theme.getColor(TAB_HOVER_BORDER); if (tabHoverBorder) { collector.addRule(` - .monaco-workbench > .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover { box-shadow: ${tabHoverBorder} 0 -1px inset !important; } `); @@ -1180,7 +1180,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const tabUnfocusedHoverBorder = theme.getColor(TAB_UNFOCUSED_HOVER_BORDER); if (tabUnfocusedHoverBorder) { collector.addRule(` - .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover { box-shadow: ${tabUnfocusedHoverBorder} 0 -1px inset !important; } `); @@ -1208,12 +1208,12 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const adjustedColor = tabHoverBackground.flatten(adjustedTabBackground); const adjustedColorDrag = tabHoverBackground.flatten(adjustedTabDragBackground); collector.addRule(` - .monaco-workbench > .part.editor > .content:not(.dragged-over) .editor-group-container.active > .title .tabs-container > .tab.sizing-shrink:not(.dragged):hover > .tab-label::after { + .monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container.active > .title .tabs-container > .tab.sizing-shrink:not(.dragged):hover > .tab-label::after { background: linear-gradient(to left, ${adjustedColor}, transparent) !important; } - .monaco-workbench > .part.editor > .content.dragged-over .editor-group-container.active > .title .tabs-container > .tab.sizing-shrink:not(.dragged):hover > .tab-label::after { + .monaco-workbench .part.editor > .content.dragged-over .editor-group-container.active > .title .tabs-container > .tab.sizing-shrink:not(.dragged):hover > .tab-label::after { background: linear-gradient(to left, ${adjustedColorDrag}, transparent) !important; } `); @@ -1224,11 +1224,11 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const adjustedColor = tabUnfocusedHoverBackground.flatten(adjustedTabBackground); const adjustedColorDrag = tabUnfocusedHoverBackground.flatten(adjustedTabDragBackground); collector.addRule(` - .monaco-workbench > .part.editor > .content:not(.dragged-over) .editor-group-container > .title .tabs-container > .tab.sizing-shrink:not(.dragged):hover > .tab-label::after { + .monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container > .title .tabs-container > .tab.sizing-shrink:not(.dragged):hover > .tab-label::after { background: linear-gradient(to left, ${adjustedColor}, transparent) !important; } - .monaco-workbench > .part.editor > .content.dragged-over .editor-group-container > .title .tabs-container > .tab.sizing-shrink:not(.dragged):hover > .tab-label::after { + .monaco-workbench .part.editor > .content.dragged-over .editor-group-container > .title .tabs-container > .tab.sizing-shrink:not(.dragged):hover > .tab-label::after { background: linear-gradient(to left, ${adjustedColorDrag}, transparent) !important; } `); @@ -1238,8 +1238,8 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { if (editorDragAndDropBackground && adjustedTabDragBackground) { const adjustedColorDrag = editorDragAndDropBackground.flatten(adjustedTabDragBackground); collector.addRule(` - .monaco-workbench > .part.editor > .content.dragged-over .editor-group-container.active > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.active):not(.dragged) > .tab-label::after, - .monaco-workbench > .part.editor > .content.dragged-over .editor-group-container:not(.active) > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.dragged) > .tab-label::after { + .monaco-workbench .part.editor > .content.dragged-over .editor-group-container.active > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.active):not(.dragged) > .tab-label::after, + .monaco-workbench .part.editor > .content.dragged-over .editor-group-container:not(.active) > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.dragged) > .tab-label::after { background: linear-gradient(to left, ${adjustedColorDrag}, transparent) !important; } `); @@ -1251,11 +1251,11 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const adjustedColor = tabActiveBackground.flatten(adjustedTabBackground); const adjustedColorDrag = tabActiveBackground.flatten(adjustedTabDragBackground); collector.addRule(` - .monaco-workbench > .part.editor > .content:not(.dragged-over) .editor-group-container > .title .tabs-container > .tab.sizing-shrink.active:not(.dragged) > .tab-label::after { + .monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container > .title .tabs-container > .tab.sizing-shrink.active:not(.dragged) > .tab-label::after { background: linear-gradient(to left, ${adjustedColor}, transparent); } - .monaco-workbench > .part.editor > .content.dragged-over .editor-group-container > .title .tabs-container > .tab.sizing-shrink.active:not(.dragged) > .tab-label::after { + .monaco-workbench .part.editor > .content.dragged-over .editor-group-container > .title .tabs-container > .tab.sizing-shrink.active:not(.dragged) > .tab-label::after { background: linear-gradient(to left, ${adjustedColorDrag}, transparent); } `); @@ -1267,11 +1267,11 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const adjustedColor = tabInactiveBackground.flatten(adjustedTabBackground); const adjustedColorDrag = tabInactiveBackground.flatten(adjustedTabDragBackground); collector.addRule(` - .monaco-workbench > .part.editor > .content:not(.dragged-over) .editor-group-container > .title .tabs-container > .tab.sizing-shrink:not(.dragged) > .tab-label::after { + .monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container > .title .tabs-container > .tab.sizing-shrink:not(.dragged) > .tab-label::after { background: linear-gradient(to left, ${adjustedColor}, transparent); } - .monaco-workbench > .part.editor > .content.dragged-over .editor-group-container > .title .tabs-container > .tab.sizing-shrink:not(.dragged) > .tab-label::after { + .monaco-workbench .part.editor > .content.dragged-over .editor-group-container > .title .tabs-container > .tab.sizing-shrink:not(.dragged) > .tab-label::after { background: linear-gradient(to left, ${adjustedColorDrag}, transparent); } `); diff --git a/src/vs/workbench/browser/parts/media/compositepart.css b/src/vs/workbench/browser/parts/media/compositepart.css index 75895952fec..fde7cdb706c 100644 --- a/src/vs/workbench/browser/parts/media/compositepart.css +++ b/src/vs/workbench/browser/parts/media/compositepart.css @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench > .part > .content > .composite { +.monaco-workbench .part > .content > .composite { height: 100%; } -.monaco-workbench > .part > .composite.title { +.monaco-workbench .part > .composite.title { display: flex; } -.monaco-workbench > .part > .composite.title > .title-actions { +.monaco-workbench .part > .composite.title > .title-actions { flex: 1; padding-left: 5px; } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/notifications/notificationsList.ts b/src/vs/workbench/browser/parts/notifications/notificationsList.ts index 08669edfbf3..fd26a6fa1d6 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsList.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsList.ts @@ -77,7 +77,8 @@ export class NotificationsList extends Themable { [renderer], { ...this.options, - setRowLineHeight: false + setRowLineHeight: false, + horizontalScrolling: false } )); diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index 18cc419df90..ce309151e69 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -3,16 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench.nopanel > .part.panel { +.monaco-workbench.nopanel .part.panel { display: none !important; visibility: hidden !important; } -.monaco-workbench > .part.panel { +.monaco-workbench .part.panel { z-index: initial; } -.monaco-workbench > .part.panel .title { +.monaco-workbench .part.panel .title { padding-right: 0px; height: 35px; display: flex; @@ -20,23 +20,23 @@ justify-content: space-between; } -.monaco-workbench > .part.panel.bottom .title { +.monaco-workbench .part.panel.bottom .title { border-top-width: 1px; border-top-style: solid; } -.monaco-workbench > .part.panel.right { +.monaco-workbench .part.panel.right { border-left-width: 1px; border-left-style: solid; } -.monaco-workbench > .part.panel > .title > .title-actions .monaco-action-bar .action-item .action-label { +.monaco-workbench .part.panel > .title > .title-actions .monaco-action-bar .action-item .action-label { outline-offset: -2px; } /** Panel Switcher */ -.monaco-workbench > .part.panel > .title > .panel-switcher-container.composite-bar > .monaco-action-bar .action-label.toggle-more { +.monaco-workbench .part.panel > .title > .panel-switcher-container.composite-bar > .monaco-action-bar .action-label.toggle-more { background-image: url('ellipsis.svg'); display: block; height: 28px; @@ -48,16 +48,16 @@ background-position: center center; } -.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar { +.monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar { line-height: 27px; /* matches panel titles in settings */ height: 35px; } -.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:first-child { +.monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:first-child { padding-left: 12px; } -.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item { +.monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item { text-transform: uppercase; padding-left: 10px; padding-right: 10px; @@ -67,24 +67,24 @@ display: flex; } -.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label{ +.monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label{ margin-right: 0; } -.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:last-child { +.monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:last-child { padding-right: 10px; } -.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item.checked .action-label { +.monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item.checked .action-label { border-bottom: 1px solid; margin-right: 0; } -.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .badge { +.monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .badge { margin-left: 8px; } -.monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .badge .badge-content { +.monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .badge .badge-content { padding: 0.3em 0.5em; border-radius: 1em; font-weight: normal; @@ -152,7 +152,7 @@ background: url('close-inverse.svg') center center no-repeat; } -.vs-dark .monaco-workbench > .part.panel > .title > .panel-switcher-container.composite-bar > .monaco-action-bar .action-label.toggle-more, -.hc-black .monaco-workbench > .part.panel > .title > .panel-switcher-container.composite-bar > .monaco-action-bar .action-label.toggle-more { +.vs-dark .monaco-workbench .part.panel > .title > .panel-switcher-container.composite-bar > .monaco-action-bar .action-label.toggle-more, +.hc-black .monaco-workbench .part.panel > .title > .panel-switcher-container.composite-bar > .monaco-action-bar .action-label.toggle-more { background-image: url('ellipsis-inverse.svg'); } diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 26a6b426ed8..68bc0f955e6 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/panelpart'; import { IAction } from 'vs/base/common/actions'; -import { Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { IPanel } from 'vs/workbench/common/panel'; @@ -32,6 +32,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ISerializableView } from 'vs/base/browser/ui/grid/grid'; export const ActivePanelContext = new RawContextKey('activePanel', ''); export const PanelFocusContext = new RawContextKey('panelFocus', false); @@ -43,7 +44,7 @@ interface ICachedPanel { visible: boolean; } -export class PanelPart extends CompositePart implements IPanelService { +export class PanelPart extends CompositePart implements IPanelService, ISerializableView { static readonly activePanelSettingsKey = 'workbench.panelpart.activepanelid'; @@ -59,6 +60,16 @@ export class PanelPart extends CompositePart implements IPanelService { private compositeActions: { [compositeId: string]: { activityAction: PanelActivityAction, pinnedAction: ToggleCompositePinnedAction } } = Object.create(null); private dimension: Dimension; + element: HTMLElement; + minimumWidth: number = 300; + maximumWidth: number = Number.POSITIVE_INFINITY; + minimumHeight: number = 77; + maximumHeight: number = Number.POSITIVE_INFINITY; + snapSize: number = 50; + + private _onDidChange = new Emitter<{ width: number; height: number; }>(); + readonly onDidChange = this._onDidChange.event; + constructor( id: string, @INotificationService notificationService: INotificationService, @@ -129,6 +140,8 @@ export class PanelPart extends CompositePart implements IPanelService { } create(parent: HTMLElement): void { + this.element = parent; + super.create(parent); const focusTracker = trackFocus(parent); @@ -271,21 +284,32 @@ export class PanelPart extends CompositePart implements IPanelService { }; } - layout(dimension: Dimension): Dimension[] { + layout(dimension: Dimension): Dimension[]; + layout(width: number, height: number): void; + layout(dim1: Dimension | number, dim2?: number): Dimension[] | void { if (!this.partService.isVisible(Parts.PANEL_PART)) { - return [dimension]; + if (dim1 instanceof Dimension) { + return [dim1]; + } + + return; } + const { width, height } = dim1 instanceof Dimension ? dim1 : { width: dim1, height: dim2 }; + if (this.partService.getPanelPosition() === Position.RIGHT) { // Take into account the 1px border when layouting - this.dimension = new Dimension(dimension.width - 1, dimension.height); + this.dimension = new Dimension(width - 1, height); } else { - this.dimension = dimension; + this.dimension = new Dimension(width, height); } - const sizes = super.layout(this.dimension); + + const sizes = super.layout(this.dimension.width, this.dimension.height); this.layoutCompositeBar(); - return sizes; + if (dim1 instanceof Dimension) { + return sizes; + } } private layoutCompositeBar(): void { @@ -408,6 +432,12 @@ export class PanelPart extends CompositePart implements IPanelService { private setStoredCachedViewletsValue(value: string): void { this.storageService.store(PanelPart.PINNED_PANELS, value, StorageScope.GLOBAL); } + + toJSON(): object { + return { + type: Parts.PANEL_PART + }; + } } registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { @@ -418,9 +448,9 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const panelBackground = theme.getColor(PANEL_BACKGROUND); if (panelBackground && panelBackground !== theme.getColor(editorBackground)) { collector.addRule(` - .monaco-workbench > .part.panel > .content .monaco-editor, - .monaco-workbench > .part.panel > .content .monaco-editor .margin, - .monaco-workbench > .part.panel > .content .monaco-editor .monaco-editor-background { + .monaco-workbench .part.panel > .content .monaco-editor, + .monaco-workbench .part.panel > .content .monaco-editor .margin, + .monaco-workbench .part.panel > .content .monaco-editor .monaco-editor-background { background-color: ${panelBackground}; } `); @@ -431,7 +461,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const titleActiveBorder = theme.getColor(PANEL_ACTIVE_TITLE_BORDER); if (titleActive || titleActiveBorder) { collector.addRule(` - .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:hover .action-label { + .monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:hover .action-label { color: ${titleActive} !important; border-bottom-color: ${titleActiveBorder} !important; } @@ -442,14 +472,14 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const focusBorderColor = theme.getColor(focusBorder); if (focusBorderColor) { collector.addRule(` - .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:focus .action-label { + .monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:focus .action-label { color: ${titleActive} !important; border-bottom-color: ${focusBorderColor} !important; border-bottom: 1px solid; } `); collector.addRule(` - .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:focus { + .monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:focus { outline: none; } `); @@ -461,8 +491,8 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const outline = theme.getColor(activeContrastBorder); collector.addRule(` - .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item.checked .action-label, - .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label:hover { + .monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item.checked .action-label, + .monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label:hover { outline-color: ${outline}; outline-width: 1px; outline-style: solid; @@ -471,7 +501,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { outline-offset: 1px; } - .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:not(.checked) .action-label:hover { + .monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:not(.checked) .action-label:hover { outline-style: dashed; } `); diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.contribution.ts b/src/vs/workbench/browser/parts/quickinput/quickInputActions.ts similarity index 100% rename from src/vs/workbench/browser/parts/quickinput/quickInput.contribution.ts rename to src/vs/workbench/browser/parts/quickinput/quickInputActions.ts diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputList.ts b/src/vs/workbench/browser/parts/quickinput/quickInputList.ts index 5d8f7aceeae..f24bb0bcc09 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInputList.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInputList.ts @@ -248,7 +248,8 @@ export class QuickInputList { identityProvider: { getId: element => element.saneLabel }, openController: { shouldOpen: () => false }, // Workaround #58124 setRowLineHeight: false, - multipleSelectionSupport: false + multipleSelectionSupport: false, + horizontalScrolling: false } as IListOptions) as WorkbenchList; this.list.getHTMLElement().id = id; this.disposables.push(this.list); diff --git a/src/vs/workbench/browser/parts/quickopen/quickopen.contribution.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenActions.ts similarity index 100% rename from src/vs/workbench/browser/parts/quickopen/quickopen.contribution.ts rename to src/vs/workbench/browser/parts/quickopen/quickOpenActions.ts diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index 9a5f5bd6e00..eb324691b34 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench > .sidebar > .content { +.monaco-workbench .sidebar > .content { overflow: hidden; } @@ -12,7 +12,7 @@ visibility: hidden !important; } -.monaco-workbench > .sidebar > .title > .title-label h2 { +.monaco-workbench .sidebar > .title > .title-label h2 { text-transform: uppercase; } diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index f357916019f..d95a8d716a4 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -25,16 +25,17 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { SIDE_BAR_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND, SIDE_BAR_BORDER } from 'vs/workbench/common/theme'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { Dimension, EventType, addDisposableListener, trackFocus } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, trackFocus, Dimension } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ISerializableView } from 'vs/base/browser/ui/grid/grid'; export const SidebarFocusContext = new RawContextKey('sideBarFocus', false); export const ActiveViewletContext = new RawContextKey('activeViewlet', ''); -export class SidebarPart extends CompositePart implements IViewletService { +export class SidebarPart extends CompositePart implements ISerializableView, IViewletService { _serviceBrand: any; static readonly activeViewletSettingsKey = 'workbench.sidebar.activeviewletid'; @@ -45,6 +46,16 @@ export class SidebarPart extends CompositePart implements IViewletServi private blockOpeningViewlet: boolean; private _onDidViewletDeregister = this._register(new Emitter()); + element: HTMLElement; + minimumWidth: number = 170; + maximumWidth: number = Number.POSITIVE_INFINITY; + minimumHeight: number = 0; + maximumHeight: number = Number.POSITIVE_INFINITY; + snapSize: number = 50; + + private _onDidChange = new Emitter<{ width: number; height: number; }>(); + readonly onDidChange = this._onDidChange.event; + constructor( id: string, @INotificationService notificationService: INotificationService, @@ -111,6 +122,8 @@ export class SidebarPart extends CompositePart implements IViewletServi } create(parent: HTMLElement): void { + this.element = parent; + super.create(parent); const focusTracker = trackFocus(parent); @@ -152,12 +165,22 @@ export class SidebarPart extends CompositePart implements IViewletServi container.style.borderLeftColor = !isPositionLeft ? borderColor : null; } - layout(dimension: Dimension): Dimension[] { + layout(dimension: Dimension): Dimension[]; + layout(width: number, height: number): void; + layout(dim1: Dimension | number, dim2?: number): Dimension[] | void { if (!this.partService.isVisible(Parts.SIDEBAR_PART)) { - return [dimension]; + if (dim1 instanceof Dimension) { + return [dim1]; + } + + return; } - return super.layout(dimension); + if (dim1 instanceof Dimension) { + return super.layout(dim1); + } + + super.layout(dim1, dim2!); } // Viewlet service @@ -237,6 +260,12 @@ export class SidebarPart extends CompositePart implements IViewletServi } } } + + toJSON(): object { + return { + type: Parts.SIDEBAR_PART + }; + } } class FocusSideBarAction extends Action { diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index c528511d2cf..8c31b3a23ec 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench > .part.statusbar { +.monaco-workbench .part.statusbar { box-sizing: border-box; cursor: default; width: 100%; @@ -11,18 +11,18 @@ font-size: 12px; } -.monaco-workbench > .part.statusbar > .statusbar-item { +.monaco-workbench .part.statusbar > .statusbar-item { display: inline-block; line-height: 22px; height: 100%; vertical-align: top; } -.monaco-workbench > .part.statusbar > .statusbar-item.has-beak { +.monaco-workbench .part.statusbar > .statusbar-item.has-beak { position: relative; } -.monaco-workbench > .part.statusbar > .statusbar-item.has-beak:before { +.monaco-workbench .part.statusbar > .statusbar-item.has-beak:before { content: ''; position: absolute; left: 11px; @@ -33,48 +33,48 @@ border-right: 5px solid transparent; } -.monaco-workbench > .part.statusbar > .statusbar-item.left > :first-child { +.monaco-workbench .part.statusbar > .statusbar-item.left > :first-child { margin-right: 5px; } -.monaco-workbench > .part.statusbar > .statusbar-item.right { +.monaco-workbench .part.statusbar > .statusbar-item.right { float: right; } -.monaco-workbench > .part.statusbar > .statusbar-item.right > :first-child { +.monaco-workbench .part.statusbar > .statusbar-item.right > :first-child { margin-left: 5px; } /* adding padding to the most left status bar item */ -.monaco-workbench > .part.statusbar > .statusbar-item.left:first-child, .monaco-workbench > .part.statusbar > .statusbar-item.right + .statusbar-item.left { +.monaco-workbench .part.statusbar > .statusbar-item.left:first-child, .monaco-workbench .part.statusbar > .statusbar-item.right + .statusbar-item.left { padding-left: 10px; } /* adding padding to the most right status bar item */ -.monaco-workbench > .part.statusbar > .statusbar-item.right:first-child { +.monaco-workbench .part.statusbar > .statusbar-item.right:first-child { padding-right: 10px; } -.monaco-workbench > .part.statusbar > .statusbar-item a { +.monaco-workbench .part.statusbar > .statusbar-item a { cursor: pointer; display: inline-block; height: 100%; } -.monaco-workbench > .part.statusbar > .statusbar-entry > span { +.monaco-workbench .part.statusbar > .statusbar-entry > span { height: 100%; } -.monaco-workbench > .part.statusbar > .statusbar-entry > span, -.monaco-workbench > .part.statusbar > .statusbar-entry > a { +.monaco-workbench .part.statusbar > .statusbar-entry > span, +.monaco-workbench .part.statusbar > .statusbar-entry > a { padding: 0 5px 0 5px; white-space: pre; /* gives some degree of styling */ } -.monaco-workbench > .part.statusbar > .statusbar-entry span.octicon { +.monaco-workbench .part.statusbar > .statusbar-entry span.octicon { text-align: center; font-size: 14px; } -.monaco-workbench > .part.statusbar > .statusbar-item a:hover { +.monaco-workbench .part.statusbar > .statusbar-item a:hover { text-decoration: none; } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 29e6b41dfdc..9fed8f11b46 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -24,20 +24,32 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { isThemeColor } from 'vs/editor/common/editorCommon'; import { Color } from 'vs/base/common/color'; -import { addClass, EventHelper, createStyleSheet, addDisposableListener } from 'vs/base/browser/dom'; +import { addClass, EventHelper, createStyleSheet, addDisposableListener, Dimension } from 'vs/base/browser/dom'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { Emitter } from 'vs/base/common/event'; +import { ISerializableView } from 'vs/base/browser/ui/grid/grid'; +import { Parts } from 'vs/workbench/services/part/common/partService'; -export class StatusbarPart extends Part implements IStatusbarService { +export class StatusbarPart extends Part implements IStatusbarService, ISerializableView { _serviceBrand: any; private static readonly PRIORITY_PROP = 'statusbar-entry-priority'; private static readonly ALIGNMENT_PROP = 'statusbar-entry-alignment'; - private statusItemsContainer: HTMLElement; + element: HTMLElement; private statusMsgDispose: IDisposable; + + minimumWidth: number = 0; + maximumWidth: number = Number.POSITIVE_INFINITY; + minimumHeight: number = 22; + maximumHeight: number = 22; + + private _onDidChange = new Emitter<{ width: number; height: number; }>(); + readonly onDidChange = this._onDidChange.event; + private styleElement: HTMLStyleElement; constructor( @@ -64,7 +76,7 @@ export class StatusbarPart extends Part implements IStatusbarService { const toDispose = item.render(el); // Insert according to priority - const container = this.statusItemsContainer; + const container = this.element; const neighbours = this.getEntries(alignment); let inserted = false; for (const neighbour of neighbours) { @@ -95,7 +107,7 @@ export class StatusbarPart extends Part implements IStatusbarService { private getEntries(alignment: StatusbarAlignment): HTMLElement[] { const entries: HTMLElement[] = []; - const container = this.statusItemsContainer; + const container = this.element; const children = container.children; for (let i = 0; i < children.length; i++) { const childElement = children.item(i); @@ -108,7 +120,7 @@ export class StatusbarPart extends Part implements IStatusbarService { } createContentArea(parent: HTMLElement): HTMLElement { - this.statusItemsContainer = parent; + this.element = parent; // Fill in initial items that were contributed from the registry const registry = Registry.as(Extensions.Statusbar); @@ -134,10 +146,10 @@ export class StatusbarPart extends Part implements IStatusbarService { const el = this.doCreateStatusItem(descriptor.alignment, descriptor.priority); this._register(item.render(el)); - this.statusItemsContainer.appendChild(el); + this.element.appendChild(el); } - return this.statusItemsContainer; + return this.element; } protected updateStyles(): void { @@ -161,7 +173,7 @@ export class StatusbarPart extends Part implements IStatusbarService { this.styleElement = createStyleSheet(container); } - this.styleElement.innerHTML = `.monaco-workbench > .part.statusbar > .statusbar-item.has-beak:before { border-bottom-color: ${backgroundColor}; }`; + this.styleElement.innerHTML = `.monaco-workbench .part.statusbar > .statusbar-item.has-beak:before { border-bottom-color: ${backgroundColor}; }`; } private doCreateStatusItem(alignment: StatusbarAlignment, priority: number = 0, extraClass?: string): HTMLElement { @@ -220,6 +232,22 @@ export class StatusbarPart extends Part implements IStatusbarService { return dispose; } + + layout(dimension: Dimension): Dimension[]; + layout(width: number, height: number): void; + layout(dim1: Dimension | number, dim2?: number): Dimension[] | void { + if (dim1 instanceof Dimension) { + return super.layout(dim1); + } else { + super.layout(new Dimension(dim1, dim2!)); + } + } + + toJSON(): object { + return { + type: Parts.STATUSBAR_PART + }; + } } let manageExtensionAction: ManageExtensionAction; @@ -336,21 +364,21 @@ class ManageExtensionAction extends Action { registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); if (statusBarItemHoverBackground) { - collector.addRule(`.monaco-workbench > .part.statusbar > .statusbar-item a:hover { background-color: ${statusBarItemHoverBackground}; }`); + collector.addRule(`.monaco-workbench .part.statusbar > .statusbar-item a:hover { background-color: ${statusBarItemHoverBackground}; }`); } const statusBarItemActiveBackground = theme.getColor(STATUS_BAR_ITEM_ACTIVE_BACKGROUND); if (statusBarItemActiveBackground) { - collector.addRule(`.monaco-workbench > .part.statusbar > .statusbar-item a:active { background-color: ${statusBarItemActiveBackground}; }`); + collector.addRule(`.monaco-workbench .part.statusbar > .statusbar-item a:active { background-color: ${statusBarItemActiveBackground}; }`); } const statusBarProminentItemBackground = theme.getColor(STATUS_BAR_PROMINENT_ITEM_BACKGROUND); if (statusBarProminentItemBackground) { - collector.addRule(`.monaco-workbench > .part.statusbar > .statusbar-item .status-bar-info { background-color: ${statusBarProminentItemBackground}; }`); + collector.addRule(`.monaco-workbench .part.statusbar > .statusbar-item .status-bar-info { background-color: ${statusBarProminentItemBackground}; }`); } const statusBarProminentItemHoverBackground = theme.getColor(STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND); if (statusBarProminentItemHoverBackground) { - collector.addRule(`.monaco-workbench > .part.statusbar > .statusbar-item a.status-bar-info:hover { background-color: ${statusBarProminentItemHoverBackground}; }`); + collector.addRule(`.monaco-workbench .part.statusbar > .statusbar-item a.status-bar-info:hover { background-color: ${statusBarProminentItemHoverBackground}; }`); } }); diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 85149003283..38794f793dd 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench > .part.titlebar { +.monaco-workbench .part.titlebar { box-sizing: border-box; width: 100%; padding: 0 70px; @@ -18,7 +18,7 @@ display: flex; } -.monaco-workbench > .part.titlebar > .titlebar-drag-region { +.monaco-workbench .part.titlebar > .titlebar-drag-region { top: 0; left: 0; display: block; @@ -29,7 +29,7 @@ -webkit-app-region: drag; } -.monaco-workbench > .part.titlebar > .window-title { +.monaco-workbench .part.titlebar > .window-title { flex: 0 1 auto; font-size: 12px; overflow: hidden; @@ -42,8 +42,8 @@ /* Windows/Linux: Rules for custom title (icon, window controls) */ -.windows > .monaco-workbench > .part.titlebar, -.linux > .monaco-workbench > .part.titlebar { +.windows > .monaco-workbench .part.titlebar, +.linux > .monaco-workbench .part.titlebar { padding: 0; height: 30px; line-height: 30px; @@ -51,17 +51,17 @@ overflow: visible; } -.windows > .monaco-workbench > .part.titlebar > .window-title, -.linux > .monaco-workbench > .part.titlebar > .window-title { +.windows > .monaco-workbench .part.titlebar > .window-title, +.linux > .monaco-workbench .part.titlebar > .window-title { cursor: default; } -.linux > .monaco-workbench > .part.titlebar > .window-title { +.linux > .monaco-workbench .part.titlebar > .window-title { font-size: inherit; } -.windows > .monaco-workbench > .part.titlebar > .resizer, -.linux > .monaco-workbench > .part.titlebar > .resizer { +.windows > .monaco-workbench .part.titlebar > .resizer, +.linux > .monaco-workbench .part.titlebar > .resizer { -webkit-app-region: no-drag; position: absolute; top: 0; @@ -69,13 +69,13 @@ height: 20%; } -.windows > .monaco-workbench.fullscreen > .part.titlebar > .resizer, -.linux > .monaco-workbench.fullscreen > .part.titlebar > .resizer { +.windows > .monaco-workbench.fullscreen .part.titlebar > .resizer, +.linux > .monaco-workbench.fullscreen .part.titlebar > .resizer { display: none; } -.monaco-workbench > .part.titlebar > .window-appicon { +.monaco-workbench .part.titlebar > .window-appicon { width: 35px; height: 100%; position: relative; @@ -87,11 +87,11 @@ flex-shrink: 0; } -.monaco-workbench.fullscreen > .part.titlebar > .window-appicon { +.monaco-workbench.fullscreen .part.titlebar > .window-appicon { display: none; } -.monaco-workbench > .part.titlebar > .window-controls-container { +.monaco-workbench .part.titlebar > .window-controls-container { display: flex; flex-grow: 0; flex-shrink: 0; @@ -104,56 +104,56 @@ margin-left: auto; } -.monaco-workbench.fullscreen > .part.titlebar > .window-controls-container { +.monaco-workbench.fullscreen .part.titlebar > .window-controls-container { display: none; } -.monaco-workbench > .part.titlebar > .window-controls-container > .window-icon-bg { +.monaco-workbench .part.titlebar > .window-controls-container > .window-icon-bg { display: inline-block; -webkit-app-region: no-drag; height: 100%; width: 33.34%; } -.monaco-workbench > .part.titlebar > .window-controls-container .window-icon svg { +.monaco-workbench .part.titlebar > .window-controls-container .window-icon svg { shape-rendering: crispEdges; text-align: center; } -.monaco-workbench > .part.titlebar.titlebar > .window-controls-container .window-close { +.monaco-workbench .part.titlebar.titlebar > .window-controls-container .window-close { -webkit-mask: url('chrome-close.svg') no-repeat 50% 50%; } -.monaco-workbench > .part.titlebar.titlebar > .window-controls-container .window-unmaximize { +.monaco-workbench .part.titlebar.titlebar > .window-controls-container .window-unmaximize { -webkit-mask: url('chrome-restore.svg') no-repeat 50% 50%; } -.monaco-workbench > .part.titlebar > .window-controls-container .window-maximize { +.monaco-workbench .part.titlebar > .window-controls-container .window-maximize { -webkit-mask: url('chrome-maximize.svg') no-repeat 50% 50%; } -.monaco-workbench > .part.titlebar > .window-controls-container .window-minimize { +.monaco-workbench .part.titlebar > .window-controls-container .window-minimize { -webkit-mask: url('chrome-minimize.svg') no-repeat 50% 50%; } -.monaco-workbench > .part.titlebar > .window-controls-container > .window-icon-bg > .window-icon { +.monaco-workbench .part.titlebar > .window-controls-container > .window-icon-bg > .window-icon { height: 100%; width: 100%; -webkit-mask-size: 23.1%; } -.monaco-workbench > .part.titlebar > .window-controls-container > .window-icon-bg:hover { +.monaco-workbench .part.titlebar > .window-controls-container > .window-icon-bg:hover { background-color: rgba(255, 255, 255, 0.1); } -.monaco-workbench > .part.titlebar.light > .window-controls-container > .window-icon-bg:hover { +.monaco-workbench .part.titlebar.light > .window-controls-container > .window-icon-bg:hover { background-color: rgba(0, 0, 0, 0.1); } -.monaco-workbench > .part.titlebar > .window-controls-container > .window-icon-bg.window-close-bg:hover { +.monaco-workbench .part.titlebar > .window-controls-container > .window-icon-bg.window-close-bg:hover { background-color: rgba(232, 17, 35, 0.9); } -.monaco-workbench > .part.titlebar > .window-controls-container .window-icon.window-close:hover { +.monaco-workbench .part.titlebar > .window-controls-container .window-icon.window-close:hover { background-color: white; } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 93d58329d84..c6bd885de3b 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -5,6 +5,7 @@ import 'vs/css!./media/titlebarpart'; import * as paths from 'vs/base/common/paths'; +import * as resources from 'vs/base/common/resources'; import { Part } from 'vs/workbench/browser/part'; import { ITitleService, ITitleProperties } from 'vs/workbench/services/title/common/titleService'; import { getZoomFactor } from 'vs/base/browser/browser'; @@ -30,10 +31,12 @@ import { MenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarContr import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { template, getBaseLabel } from 'vs/base/common/labels'; import { ILabelService } from 'vs/platform/label/common/label'; -import { Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ISerializableView } from 'vs/base/browser/ui/grid/grid'; +import { Parts } from 'vs/workbench/services/part/common/partService'; -export class TitlebarPart extends Part implements ITitleService { +export class TitlebarPart extends Part implements ITitleService, ISerializableView { _serviceBrand: any; @@ -43,7 +46,7 @@ export class TitlebarPart extends Part implements ITitleService { private static readonly TITLE_DIRTY = '\u25cf '; private static readonly TITLE_SEPARATOR = isMacintosh ? ' — ' : ' - '; // macOS uses special - separator - private titleContainer: HTMLElement; + element: HTMLElement; private title: HTMLElement; private dragRegion: HTMLElement; private windowControls: HTMLElement; @@ -61,6 +64,14 @@ export class TitlebarPart extends Part implements ITitleService { private properties: ITitleProperties; private activeEditorListeners: IDisposable[]; + minimumWidth: number = 0; + maximumWidth: number = Number.POSITIVE_INFINITY; + minimumHeight: number = isMacintosh ? 22 : 30; + maximumHeight: number = isMacintosh ? 22 : 30; + + private _onDidChange = new Emitter<{ width: number; height: number; }>(); + readonly onDidChange = this._onDidChange.event; + constructor( id: string, @IContextMenuService private readonly contextMenuService: IContextMenuService, @@ -228,21 +239,25 @@ export class TitlebarPart extends Part implements ITitleService { /** * Possible template values: * - * {activeEditorLong}: e.g. /Users/Development/myProject/myFolder/myFile.txt - * {activeEditorMedium}: e.g. myFolder/myFile.txt + * {activeEditorLong}: e.g. /Users/Development/myFolder/myFileFolder/myFile.txt + * {activeEditorMedium}: e.g. myFolder/myFileFolder/myFile.txt * {activeEditorShort}: e.g. myFile.txt + * {activeFolderLong}: e.g. /Users/Development/myFolder/myFileFolder + * {activeFolderMedium}: e.g. myFolder/myFileFolder + * {activeFolderShort}: e.g. myFileFolder * {rootName}: e.g. myFolder1, myFolder2, myFolder3 - * {rootPath}: e.g. /Users/Development/myProject + * {rootPath}: e.g. /Users/Development * {folderName}: e.g. myFolder * {folderPath}: e.g. /Users/Development/myFolder * {appName}: e.g. VS Code - * {dirty}: indiactor + * {dirty}: indicator * {separator}: conditional separator */ private doGetWindowTitle(): string { const editor = this.editorService.activeEditor; const workspace = this.contextService.getWorkspace(); + // Compute root let root: URI; if (workspace.configuration) { root = workspace.configuration; @@ -250,15 +265,25 @@ export class TitlebarPart extends Part implements ITitleService { root = workspace.folders[0].uri; } + // Compute active editor folder + const editorResource = editor ? toResource(editor) : undefined; + let editorFolderResource = editorResource ? resources.dirname(editorResource) : undefined; + if (editorFolderResource && editorFolderResource.path === '.') { + editorFolderResource = undefined; + } + // Compute folder resource // Single Root Workspace: always the root single workspace in this case // Otherwise: root folder of the currently active file if any - let folder = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER ? workspace.folders[0] : this.contextService.getWorkspaceFolder(toResource(editor, { supportSideBySide: true })); + const folder = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER ? workspace.folders[0] : this.contextService.getWorkspaceFolder(toResource(editor, { supportSideBySide: true })); // Variables const activeEditorShort = editor ? editor.getTitle(Verbosity.SHORT) : ''; const activeEditorMedium = editor ? editor.getTitle(Verbosity.MEDIUM) : activeEditorShort; const activeEditorLong = editor ? editor.getTitle(Verbosity.LONG) : activeEditorMedium; + const activeFolderShort = editorFolderResource ? resources.basename(editorFolderResource) : ''; + const activeFolderMedium = editorFolderResource ? this.labelService.getUriLabel(editorFolderResource, { relative: true }) : ''; + const activeFolderLong = editorFolderResource ? this.labelService.getUriLabel(editorFolderResource) : ''; const rootName = this.labelService.getWorkspaceLabel(workspace); const rootPath = root ? this.labelService.getUriLabel(root) : ''; const folderName = folder ? folder.name : ''; @@ -272,6 +297,9 @@ export class TitlebarPart extends Part implements ITitleService { activeEditorShort, activeEditorLong, activeEditorMedium, + activeFolderShort, + activeFolderMedium, + activeFolderLong, rootName, rootPath, folderName, @@ -283,14 +311,14 @@ export class TitlebarPart extends Part implements ITitleService { } createContentArea(parent: HTMLElement): HTMLElement { - this.titleContainer = parent; + this.element = parent; // Draggable region that we can manipulate for #52522 - this.dragRegion = append(this.titleContainer, $('div.titlebar-drag-region')); + this.dragRegion = append(this.element, $('div.titlebar-drag-region')); // App Icon (Windows/Linux) if (!isMacintosh) { - this.appIcon = append(this.titleContainer, $('div.window-appicon')); + this.appIcon = append(this.element, $('div.window-appicon')); this.onUpdateAppIconDragBehavior(); this._register(addDisposableListener(this.appIcon, EventType.DBLCLICK, (e => { @@ -300,7 +328,7 @@ export class TitlebarPart extends Part implements ITitleService { // Menubar: the menubar part which is responsible for populating both the custom and native menubars this.menubarPart = this.instantiationService.createInstance(MenubarControl); - this.menubar = append(this.titleContainer, $('div.menubar')); + this.menubar = append(this.element, $('div.menubar')); this.menubar.setAttribute('role', 'menubar'); this.menubarPart.create(this.menubar); @@ -311,7 +339,7 @@ export class TitlebarPart extends Part implements ITitleService { } // Title - this.title = append(this.titleContainer, $('div.window-title')); + this.title = append(this.element, $('div.window-title')); if (this.pendingTitle) { this.title.innerText = this.pendingTitle; } else { @@ -320,7 +348,7 @@ export class TitlebarPart extends Part implements ITitleService { // Maximize/Restore on doubleclick if (isMacintosh) { - this._register(addDisposableListener(this.titleContainer, EventType.DBLCLICK, e => { + this._register(addDisposableListener(this.element, EventType.DBLCLICK, e => { EventHelper.stop(e); this.onTitleDoubleclick(); @@ -340,7 +368,7 @@ export class TitlebarPart extends Part implements ITitleService { // Window Controls (Windows/Linux) if (!isMacintosh) { - this.windowControls = append(this.titleContainer, $('div.window-controls-container')); + this.windowControls = append(this.element, $('div.window-controls-container')); // Minimize @@ -375,7 +403,7 @@ export class TitlebarPart extends Part implements ITitleService { })); // Resizer - this.resizer = append(this.titleContainer, $('div.resizer')); + this.resizer = append(this.element, $('div.resizer')); const isMaximized = this.windowService.getConfiguration().maximized ? true : false; this.onDidChangeMaximized(isMaximized); @@ -384,7 +412,7 @@ export class TitlebarPart extends Part implements ITitleService { // Since the title area is used to drag the window, we do not want to steal focus from the // currently active element. So we restore focus after a timeout back to where it was. - this._register(addDisposableListener(this.titleContainer, EventType.MOUSE_DOWN, e => { + this._register(addDisposableListener(this.element, EventType.MOUSE_DOWN, e => { if (e.target && isAncestor(e.target as HTMLElement, this.menubar)) { return; } @@ -399,7 +427,7 @@ export class TitlebarPart extends Part implements ITitleService { this.updateStyles(); - return this.titleContainer; + return this.element; } private onDidChangeMaximized(maximized: boolean) { @@ -428,26 +456,26 @@ export class TitlebarPart extends Part implements ITitleService { super.updateStyles(); // Part container - if (this.titleContainer) { + if (this.element) { if (this.isInactive) { - addClass(this.titleContainer, 'inactive'); + addClass(this.element, 'inactive'); } else { - removeClass(this.titleContainer, 'inactive'); + removeClass(this.element, 'inactive'); } const titleBackground = this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_BACKGROUND : TITLE_BAR_ACTIVE_BACKGROUND); - this.titleContainer.style.backgroundColor = titleBackground; + this.element.style.backgroundColor = titleBackground; if (Color.fromHex(titleBackground).isLighter()) { - addClass(this.titleContainer, 'light'); + addClass(this.element, 'light'); } else { - removeClass(this.titleContainer, 'light'); + removeClass(this.element, 'light'); } const titleForeground = this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_FOREGROUND : TITLE_BAR_ACTIVE_FOREGROUND); - this.titleContainer.style.color = titleForeground; + this.element.style.color = titleForeground; const titleBorder = this.getColor(TITLE_BAR_BORDER); - this.titleContainer.style.borderBottom = titleBorder ? `1px solid ${titleBorder}` : null; + this.element.style.borderBottom = titleBorder ? `1px solid ${titleBorder}` : null; } } @@ -513,8 +541,8 @@ export class TitlebarPart extends Part implements ITitleService { private adjustTitleMarginToCenter(): void { if (!isMacintosh && - (this.appIcon.clientWidth + this.menubar.clientWidth + 10 > (this.titleContainer.clientWidth - this.title.clientWidth) / 2 || - this.titleContainer.clientWidth - this.windowControls.clientWidth - 10 < (this.titleContainer.clientWidth + this.title.clientWidth) / 2)) { + (this.appIcon.clientWidth + this.menubar.clientWidth + 10 > (this.element.clientWidth - this.title.clientWidth) / 2 || + this.element.clientWidth - this.windowControls.clientWidth - 10 < (this.element.clientWidth + this.title.clientWidth) / 2)) { this.title.style.position = null; this.title.style.left = null; this.title.style.transform = null; @@ -525,7 +553,7 @@ export class TitlebarPart extends Part implements ITitleService { } } - layout(dimension: Dimension): Dimension[] { + updateLayout(dimension: Dimension): void { if (getTitleBarStyle(this.configurationService, this.environmentService) === 'custom') { // Only prevent zooming behavior on macOS or when the menubar is not visible if (isMacintosh || this.configurationService.getValue('window.menuBarVisibility') === 'hidden') { @@ -549,8 +577,27 @@ export class TitlebarPart extends Part implements ITitleService { this.menubarPart.layout(menubarDimension); } } + } - return super.layout(dimension); + layout(dimension: Dimension): Dimension[]; + layout(width: number, height: number): void; + layout(dim1: Dimension | number, dim2?: number): Dimension[] | void { + if (dim1 instanceof Dimension) { + this.updateLayout(dim1); + + return super.layout(dim1); + } + + const dimensions = new Dimension(dim1, dim2); + this.updateLayout(dimensions); + + super.layout(dimensions); + } + + toJSON(): object { + return { + type: Parts.TITLEBAR_PART + }; } } @@ -569,7 +616,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const titlebarActiveFg = theme.getColor(TITLE_BAR_ACTIVE_FOREGROUND); if (titlebarActiveFg) { collector.addRule(` - .monaco-workbench > .part.titlebar > .window-controls-container .window-icon { + .monaco-workbench .part.titlebar > .window-controls-container .window-icon { background-color: ${titlebarActiveFg}; } `); @@ -578,7 +625,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const titlebarInactiveFg = theme.getColor(TITLE_BAR_INACTIVE_FOREGROUND); if (titlebarInactiveFg) { collector.addRule(` - .monaco-workbench > .part.titlebar.inactive > .window-controls-container .window-icon { + .monaco-workbench .part.titlebar.inactive > .window-controls-container .window-icon { background-color: ${titlebarInactiveFg}; } `); diff --git a/src/vs/workbench/browser/parts/views/panelViewlet.ts b/src/vs/workbench/browser/parts/views/panelViewlet.ts index 5979e6d8b44..8622e66392e 100644 --- a/src/vs/workbench/browser/parts/views/panelViewlet.ts +++ b/src/vs/workbench/browser/parts/views/panelViewlet.ts @@ -259,7 +259,7 @@ export class PanelViewlet extends Viewlet { let title = Registry.as(Extensions.Viewlets).getViewlet(this.getId()).name; if (this.isSingleView()) { - title += ': ' + this.panelItems[0].panel.title; + title = `${title}: ${this.panelItems[0].panel.title}`; } return title; @@ -305,7 +305,7 @@ export class PanelViewlet extends Viewlet { } layout(dimension: Dimension): void { - this.panelview.layout(dimension.height); + this.panelview.layout(dimension.height, dimension.width); } getOptimalWidth(): number { diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index f843c9bc95f..f071f08339a 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -12,13 +12,12 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { Composite, CompositeDescriptor, CompositeRegistry } from 'vs/workbench/browser/composite'; import { IConstructorSignature0 } from 'vs/platform/instantiation/common/instantiation'; -import { ToggleSidebarVisibilityAction } from 'vs/workbench/browser/actions/toggleSidebarVisibility'; +import { ToggleSidebarVisibilityAction, ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; import { URI } from 'vs/base/common/uri'; -import { ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/toggleSidebarPosition'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts new file mode 100644 index 00000000000..3fcab416418 --- /dev/null +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -0,0 +1,287 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Registry } from 'vs/platform/registry/common/platform'; +import * as nls from 'vs/nls'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { isMacintosh } from 'vs/base/common/platform'; + +const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); + +// Configuration: Workbench +configurationRegistry.registerConfiguration({ + 'id': 'workbench', + 'order': 7, + 'title': nls.localize('workbenchConfigurationTitle', "Workbench"), + 'type': 'object', + 'properties': { + 'workbench.editor.showTabs': { + 'type': 'boolean', + 'description': nls.localize('showEditorTabs', "Controls whether opened editors should show in tabs or not."), + 'default': true + }, + 'workbench.editor.highlightModifiedTabs': { + 'type': 'boolean', + 'description': nls.localize('highlightModifiedTabs', "Controls whether a top border is drawn on modified (dirty) editor tabs or not."), + 'default': false + }, + 'workbench.editor.labelFormat': { + 'type': 'string', + 'enum': ['default', 'short', 'medium', 'long'], + 'enumDescriptions': [ + nls.localize('workbench.editor.labelFormat.default', "Show the name of the file. When tabs are enabled and two files have the same name in one group the distinguishing sections of each file's path are added. When tabs are disabled, the path relative to the workspace folder is shown if the editor is active."), + nls.localize('workbench.editor.labelFormat.short', "Show the name of the file followed by its directory name."), + nls.localize('workbench.editor.labelFormat.medium', "Show the name of the file followed by its path relative to the workspace folder."), + nls.localize('workbench.editor.labelFormat.long', "Show the name of the file followed by its absolute path.") + ], + 'default': 'default', + 'description': nls.localize({ + comment: ['This is the description for a setting. Values surrounded by parenthesis are not to be translated.'], + key: 'tabDescription' + }, "Controls the format of the label for an editor."), + }, + 'workbench.editor.tabCloseButton': { + 'type': 'string', + 'enum': ['left', 'right', 'off'], + 'default': 'right', + 'description': 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'.") + }, + 'workbench.editor.tabSizing': { + 'type': 'string', + 'enum': ['fit', 'shrink'], + 'default': 'fit', + 'enumDescriptions': [ + 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.") + ], + 'description': 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.") + }, + 'workbench.editor.closeTabsInMRUOrder': { + 'type': 'boolean', + 'description': nls.localize('closeTabsInMRUOrder', "Controls whether tabs are closed in most recently used order or from left to right."), + 'default': true + }, + 'workbench.editor.showIcons': { + 'type': 'boolean', + 'description': nls.localize('showIcons', "Controls whether opened editors should show with an icon or not. This requires an icon theme to be enabled as well."), + 'default': true + }, + 'workbench.editor.enablePreview': { + 'type': 'boolean', + 'description': nls.localize('enablePreview', "Controls whether opened editors show as preview. Preview editors are reused until they are pinned (e.g. via double click or editing) and show up with an italic font style."), + 'default': true + }, + 'workbench.editor.enablePreviewFromQuickOpen': { + 'type': 'boolean', + 'description': nls.localize('enablePreviewFromQuickOpen', "Controls whether opened editors from Quick Open show as preview. Preview editors are reused until they are pinned (e.g. via double click or editing)."), + 'default': true + }, + 'workbench.editor.closeOnFileDelete': { + 'type': 'boolean', + 'description': nls.localize('closeOnFileDelete', "Controls whether editors showing a file that was opened during the session should close automatically when getting deleted or renamed by some other process. Disabling this will keep the editor open on such an event. Note that deleting from within the application will always close the editor and that dirty files will never close to preserve your data."), + 'default': false + }, + 'workbench.editor.openPositioning': { + 'type': 'string', + 'enum': ['left', 'right', 'first', 'last'], + 'default': 'right', + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'editorOpenPositioning' }, "Controls where editors open. Select `left` or `right` to open editors to the left or right of the currently active one. Select `first` or `last` to open editors independently from the currently active one.") + }, + 'workbench.editor.openSideBySideDirection': { + 'type': 'string', + 'enum': ['right', 'down'], + 'default': 'right', + 'markdownDescription': nls.localize('sideBySideDirection', "Controls the default direction of editors that are opened side by side (e.g. from the explorer). By default, editors will open on the right hand side of the currently active one. If changed to `down`, the editors will open below the currently active one.") + }, + 'workbench.editor.closeEmptyGroups': { + 'type': 'boolean', + 'description': nls.localize('closeEmptyGroups', "Controls the behavior of empty editor groups when the last tab in the group is closed. When enabled, empty groups will automatically close. When disabled, empty groups will remain part of the grid."), + 'default': true + }, + 'workbench.editor.revealIfOpen': { + 'type': 'boolean', + 'description': nls.localize('revealIfOpen', "Controls whether an editor is revealed in any of the visible groups if opened. If disabled, an editor will prefer to open in the currently active editor group. If enabled, an already opened editor will be revealed instead of opened again in the currently active editor group. Note that there are some cases where this setting is ignored, e.g. when forcing an editor to open in a specific group or to the side of the currently active group."), + 'default': false + }, + 'workbench.editor.swipeToNavigate': { + 'type': 'boolean', + 'description': nls.localize('swipeToNavigate', "Navigate between open files using three-finger swipe horizontally."), + 'default': false, + 'included': isMacintosh + }, + 'workbench.editor.restoreViewState': { + 'type': 'boolean', + 'description': nls.localize('restoreViewState', "Restores the last view state (e.g. scroll position) when re-opening files after they have been closed."), + 'default': true, + }, + 'workbench.editor.centeredLayoutAutoResize': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('centeredLayoutAutoResize', "Controls if the centered layout should automatically resize to maximum width when more than one group is open. Once only one group is open it will resize back to the original centered width.") + }, + 'workbench.commandPalette.history': { + 'type': 'number', + 'description': nls.localize('commandHistory', "Controls the number of recently used commands to keep in history for the command palette. Set to 0 to disable command history."), + 'default': 50 + }, + 'workbench.commandPalette.preserveInput': { + 'type': 'boolean', + 'description': nls.localize('preserveInput', "Controls whether the last typed input to the command palette should be restored when opening it the next time."), + 'default': false + }, + 'workbench.quickOpen.closeOnFocusLost': { + 'type': 'boolean', + 'description': nls.localize('closeOnFocusLost', "Controls whether Quick Open should close automatically once it loses focus."), + 'default': true + }, + 'workbench.quickOpen.preserveInput': { + 'type': 'boolean', + 'description': nls.localize('workbench.quickOpen.preserveInput', "Controls whether the last typed input to Quick Open should be restored when opening it the next time."), + 'default': false + }, + 'workbench.settings.openDefaultSettings': { + 'type': 'boolean', + 'description': nls.localize('openDefaultSettings', "Controls whether opening settings also opens an editor showing all default settings."), + 'default': false + }, + 'workbench.settings.useSplitJSON': { + 'type': 'boolean', + 'markdownDescription': nls.localize('useSplitJSON', "Controls whether to use the split JSON editor when editing settings as JSON."), + 'default': false + }, + 'workbench.settings.openDefaultKeybindings': { + 'type': 'boolean', + 'description': nls.localize('openDefaultKeybindings', "Controls whether opening keybinding settings also opens an editor showing all default keybindings."), + 'default': true + }, + 'workbench.sideBar.location': { + 'type': 'string', + 'enum': ['left', 'right'], + 'default': 'left', + 'description': nls.localize('sideBarLocation', "Controls the location of the sidebar. It can either show on the left or right of the workbench.") + }, + 'workbench.panel.defaultLocation': { + 'type': 'string', + 'enum': ['bottom', 'right'], + 'default': 'bottom', + 'description': nls.localize('panelDefaultLocation', "Controls the default location of the panel (terminal, debug console, output, problems). It can either show at the bottom or on the right of the workbench.") + }, + 'workbench.statusBar.visible': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('statusBarVisibility', "Controls the visibility of the status bar at the bottom of the workbench.") + }, + 'workbench.activityBar.visible': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('activityBarVisibility', "Controls the visibility of the activity bar in the workbench.") + }, + 'workbench.view.alwaysShowHeaderActions': { + 'type': 'boolean', + 'default': false, + 'description': nls.localize('viewVisibility', "Controls the visibility of view header actions. View header actions may either be always visible, or only visible when that view is focused or hovered over.") + }, + 'workbench.fontAliasing': { + 'type': 'string', + 'enum': ['default', 'antialiased', 'none', 'auto'], + 'default': 'default', + 'description': + nls.localize('fontAliasing', "Controls font aliasing method in the workbench."), + 'enumDescriptions': [ + nls.localize('workbench.fontAliasing.default', "Sub-pixel font smoothing. On most non-retina displays this will give the sharpest text."), + nls.localize('workbench.fontAliasing.antialiased', "Smooth the font on the level of the pixel, as opposed to the subpixel. Can make the font appear lighter overall."), + nls.localize('workbench.fontAliasing.none', "Disables font smoothing. Text will show with jagged sharp edges."), + nls.localize('workbench.fontAliasing.auto', "Applies `default` or `antialiased` automatically based on the DPI of displays.") + ], + 'included': isMacintosh + }, + 'workbench.settings.enableNaturalLanguageSearch': { + 'type': 'boolean', + 'description': nls.localize('enableNaturalLanguageSettingsSearch', "Controls whether to enable the natural language search mode for settings. The natural language search is provided by a Microsoft online service."), + 'default': true, + 'scope': ConfigurationScope.WINDOW, + 'tags': ['usesOnlineServices'] + }, + 'workbench.settings.settingsSearchTocBehavior': { + 'type': 'string', + 'enum': ['hide', 'filter'], + 'enumDescriptions': [ + nls.localize('settingsSearchTocBehavior.hide', "Hide the Table of Contents while searching."), + nls.localize('settingsSearchTocBehavior.filter', "Filter the Table of Contents to just categories that have matching settings. Clicking a category will filter the results to that category."), + ], + 'description': nls.localize('settingsSearchTocBehavior', "Controls the behavior of the settings editor Table of Contents while searching."), + 'default': 'filter', + 'scope': ConfigurationScope.WINDOW + }, + 'workbench.settings.editor': { + 'type': 'string', + 'enum': ['ui', 'json'], + 'enumDescriptions': [ + nls.localize('settings.editor.ui', "Use the settings UI editor."), + nls.localize('settings.editor.json', "Use the JSON file editor."), + ], + 'description': nls.localize('settings.editor.desc', "Determines which settings editor to use by default."), + 'default': 'ui', + 'scope': ConfigurationScope.WINDOW + }, + 'workbench.enableExperiments': { + 'type': 'boolean', + 'description': nls.localize('workbench.enableExperiments', "Fetches experiments to run from a Microsoft online service."), + 'default': true, + 'tags': ['usesOnlineServices'] + }, + 'workbench.useExperimentalGridLayout': { + 'type': 'boolean', + 'description': nls.localize('workbench.useExperimentalGridLayout', "Enables the grid layout for the workbench. This setting may enable additional layout options for workbench components."), + 'default': false, + 'scope': ConfigurationScope.APPLICATION + } + } +}); + +// Configuration: Zen Mode +configurationRegistry.registerConfiguration({ + 'id': 'zenMode', + 'order': 9, + 'title': nls.localize('zenModeConfigurationTitle', "Zen Mode"), + 'type': 'object', + 'properties': { + 'zenMode.fullScreen': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('zenMode.fullScreen', "Controls whether turning on Zen Mode also puts the workbench into full screen mode.") + }, + 'zenMode.centerLayout': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('zenMode.centerLayout', "Controls whether turning on Zen Mode also centers the layout.") + }, + 'zenMode.hideTabs': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('zenMode.hideTabs', "Controls whether turning on Zen Mode also hides workbench tabs.") + }, + 'zenMode.hideStatusBar': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('zenMode.hideStatusBar', "Controls whether turning on Zen Mode also hides the status bar at the bottom of the workbench.") + }, + 'zenMode.hideActivityBar': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('zenMode.hideActivityBar', "Controls whether turning on Zen Mode also hides the activity bar at the left of the workbench.") + }, + 'zenMode.hideLineNumbers': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('zenMode.hideLineNumbers', "Controls whether turning on Zen Mode also hides the editor line numbers.") + }, + 'zenMode.restore': { + 'type': 'boolean', + 'default': false, + 'description': nls.localize('zenMode.restore', "Controls whether a window should restore to zen mode if it was exited in zen mode.") + } + } +}); \ No newline at end of file diff --git a/src/vs/workbench/buildfile.js b/src/vs/workbench/buildfile.js index b9f60119de5..8946ef5adb8 100644 --- a/src/vs/workbench/buildfile.js +++ b/src/vs/workbench/buildfile.js @@ -22,11 +22,11 @@ exports.collectModules = function () { createModuleDescription('vs/workbench/parts/debug/node/telemetryApp', []), createModuleDescription('vs/workbench/services/search/node/searchApp', []), - createModuleDescription('vs/workbench/services/search/node/legacy/worker/searchWorkerApp', []), + createModuleDescription('vs/workbench/services/files/node/watcher/unix/watcherApp', []), createModuleDescription('vs/workbench/services/files/node/watcher/nsfw/watcherApp', []), - createModuleDescription('vs/workbench/node/extensionHostProcess', []), + createModuleDescription('vs/workbench/services/extensions/node/extensionHostProcess', []), ]; return modules; diff --git a/src/vs/workbench/common/contributions.ts b/src/vs/workbench/common/contributions.ts index f881e90555e..01a8631bf8f 100644 --- a/src/vs/workbench/common/contributions.ts +++ b/src/vs/workbench/common/contributions.ts @@ -8,8 +8,6 @@ import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/ import { Registry } from 'vs/platform/registry/common/platform'; import { runWhenIdle, IdleDeadline } from 'vs/base/common/async'; -// --- Workbench Contribution Registry - /** * A workbench contribution that will be loaded when the workbench starts and disposed when the workbench shuts down. */ diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index a5ffddb8e59..c5fc0beab31 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -298,6 +298,11 @@ export const STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND = registerColor('statusB hc: '#369432' }, nls.localize('statusBarProminentItemHoverBackground', "Status bar prominent items background color when hovering. Prominent items stand out from other status bar entries to indicate importance. Change mode `Toggle Tab Key Moves Focus` from command palette to see an example. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_HOST_NAME_BACKGROUND = registerColor('statusBarItem.hostBackground', { + dark: STATUS_BAR_PROMINENT_ITEM_BACKGROUND, + light: STATUS_BAR_PROMINENT_ITEM_BACKGROUND, + hc: STATUS_BAR_PROMINENT_ITEM_BACKGROUND +}, nls.localize('statusBarItemHostBackground', "Background color for the remote host name on the status bar.")); // < --- Activity Bar --- > diff --git a/src/vs/workbench/electron-browser/actions.ts b/src/vs/workbench/electron-browser/actions.ts deleted file mode 100644 index a2538660366..00000000000 --- a/src/vs/workbench/electron-browser/actions.ts +++ /dev/null @@ -1,1412 +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 'vs/css!./media/actions'; - -import { URI } from 'vs/base/common/uri'; -import { Action } from 'vs/base/common/actions'; -import { IWindowService, IWindowsService, MenuBarVisibility } from 'vs/platform/windows/common/windows'; -import * as nls from 'vs/nls'; -import product from 'vs/platform/node/product'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; -import { isMacintosh, isLinux, language } from 'vs/base/common/platform'; -import * as browser from 'vs/base/browser/browser'; -import { IEditorGroupsService, GroupDirection, GroupLocation, IFindGroupScope } from 'vs/workbench/services/group/common/editorGroupsService'; -import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { IPartService, Parts, Position as PartPosition } from 'vs/workbench/services/part/common/partService'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { webFrame, shell } from 'electron'; -import { getBaseLabel } from 'vs/base/common/labels'; -import { IViewlet } from 'vs/workbench/common/viewlet'; -import { IPanel } from 'vs/workbench/common/panel'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { FileKind } from 'vs/platform/files/common/files'; -import { IssueType } from 'vs/platform/issue/common/issue'; -import { domEvent } from 'vs/base/browser/event'; -import { Event } from 'vs/base/common/event'; -import { IDisposable, toDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; -import { getDomNodePagePosition, createStyleSheet, createCSSRule, append, $ } from 'vs/base/browser/dom'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { Context } from 'vs/platform/contextkey/browser/contextKeyService'; -import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { dirname } from 'vs/base/common/resources'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { IQuickInputService, IQuickPickItem, IQuickInputButton, IQuickPickSeparator, IKeyMods } from 'vs/platform/quickinput/common/quickInput'; -import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { timeout } from 'vs/base/common/async'; - -// --- actions - -export class CloseCurrentWindowAction extends Action { - - static readonly ID = 'workbench.action.closeWindow'; - static readonly LABEL = nls.localize('closeWindow', "Close Window"); - - constructor(id: string, label: string, @IWindowService private readonly windowService: IWindowService) { - super(id, label); - } - - run(): Promise { - this.windowService.closeWindow(); - - return Promise.resolve(true); - } -} - -export class CloseWorkspaceAction extends Action { - - static readonly ID = 'workbench.action.closeFolder'; - static LABEL = nls.localize('closeWorkspace', "Close Workspace"); - - constructor( - id: string, - label: string, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @INotificationService private readonly notificationService: INotificationService, - @IWindowService private readonly windowService: IWindowService - ) { - super(id, label); - } - - 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.")); - - return Promise.resolve(undefined); - } - - return this.windowService.closeWorkspace(); - } -} - -export class NewWindowAction extends Action { - - static readonly ID = 'workbench.action.newWindow'; - static LABEL = nls.localize('newWindow', "New Window"); - - constructor( - id: string, - label: string, - @IWindowsService private readonly windowsService: IWindowsService - ) { - super(id, label); - } - - run(): Promise { - return this.windowsService.openNewWindow(); - } -} - -export class ToggleFullScreenAction extends Action { - - static readonly ID = 'workbench.action.toggleFullScreen'; - static LABEL = nls.localize('toggleFullScreen', "Toggle Full Screen"); - - constructor(id: string, label: string, @IWindowService private readonly windowService: IWindowService) { - super(id, label); - } - - run(): Promise { - return this.windowService.toggleFullScreen(); - } -} - -export class ToggleMenuBarAction extends Action { - - static readonly ID = 'workbench.action.toggleMenuBar'; - static LABEL = nls.localize('toggleMenuBar', "Toggle Menu Bar"); - - private static readonly menuBarVisibilityKey = 'window.menuBarVisibility'; - - constructor( - id: string, - label: string, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label); - } - - run(): Promise { - let currentVisibilityValue = this.configurationService.getValue(ToggleMenuBarAction.menuBarVisibilityKey); - if (typeof currentVisibilityValue !== 'string') { - currentVisibilityValue = 'default'; - } - - let newVisibilityValue: string; - if (currentVisibilityValue === 'visible' || currentVisibilityValue === 'default') { - newVisibilityValue = 'toggle'; - } else { - newVisibilityValue = 'default'; - } - - this.configurationService.updateValue(ToggleMenuBarAction.menuBarVisibilityKey, newVisibilityValue, ConfigurationTarget.USER); - - return Promise.resolve(); - } -} - -export class ToggleDevToolsAction extends Action { - - static readonly ID = 'workbench.action.toggleDevTools'; - static LABEL = nls.localize('toggleDevTools', "Toggle Developer Tools"); - - constructor(id: string, label: string, @IWindowService private readonly windowsService: IWindowService) { - super(id, label); - } - - run(): Promise { - return this.windowsService.toggleDevTools(); - } -} - -export abstract class BaseZoomAction extends Action { - private static readonly SETTING_KEY = 'window.zoomLevel'; - - constructor( - id: string, - label: string, - @IWorkspaceConfigurationService private readonly configurationService: IWorkspaceConfigurationService - ) { - super(id, label); - } - - protected setConfiguredZoomLevel(level: number): void { - level = Math.round(level); // when reaching smallest zoom, prevent fractional zoom levels - - const applyZoom = () => { - webFrame.setZoomLevel(level); - browser.setZoomFactor(webFrame.getZoomFactor()); - // See https://github.com/Microsoft/vscode/issues/26151 - // Cannot be trusted because the webFrame might take some time - // until it really applies the new zoom level - browser.setZoomLevel(webFrame.getZoomLevel(), /*isTrusted*/false); - }; - - this.configurationService.updateValue(BaseZoomAction.SETTING_KEY, level).then(() => applyZoom()); - } -} - -export class ZoomInAction extends BaseZoomAction { - - static readonly ID = 'workbench.action.zoomIn'; - static readonly LABEL = nls.localize('zoomIn', "Zoom In"); - - constructor( - id: string, - label: string, - @IWorkspaceConfigurationService configurationService: IWorkspaceConfigurationService - ) { - super(id, label, configurationService); - } - - run(): Promise { - this.setConfiguredZoomLevel(webFrame.getZoomLevel() + 1); - - return Promise.resolve(true); - } -} - -export class ZoomOutAction extends BaseZoomAction { - - static readonly ID = 'workbench.action.zoomOut'; - static readonly LABEL = nls.localize('zoomOut', "Zoom Out"); - - constructor( - id: string, - label: string, - @IWorkspaceConfigurationService configurationService: IWorkspaceConfigurationService - ) { - super(id, label, configurationService); - } - - run(): Promise { - this.setConfiguredZoomLevel(webFrame.getZoomLevel() - 1); - - return Promise.resolve(true); - } -} - -export class ZoomResetAction extends BaseZoomAction { - - static readonly ID = 'workbench.action.zoomReset'; - static readonly LABEL = nls.localize('zoomReset', "Reset Zoom"); - - constructor( - id: string, - label: string, - @IWorkspaceConfigurationService configurationService: IWorkspaceConfigurationService - ) { - super(id, label, configurationService); - } - - run(): Promise { - this.setConfiguredZoomLevel(0); - - return Promise.resolve(true); - } -} - -export class ReloadWindowAction extends Action { - - static readonly ID = 'workbench.action.reloadWindow'; - static LABEL = nls.localize('reloadWindow', "Reload Window"); - - constructor( - id: string, - label: string, - @IWindowService private readonly windowService: IWindowService - ) { - super(id, label); - } - - run(): Promise { - return this.windowService.reloadWindow().then(() => true); - } -} - -export class ReloadWindowWithExtensionsDisabledAction extends Action { - - static readonly ID = 'workbench.action.reloadWindowWithExtensionsDisabled'; - static LABEL = nls.localize('reloadWindowWithExntesionsDisabled', "Reload Window With Extensions Disabled"); - - constructor( - id: string, - label: string, - @IWindowService private readonly windowService: IWindowService - ) { - super(id, label); - } - - run(): Promise { - return this.windowService.reloadWindow({ _: [], 'disable-extensions': true }).then(() => true); - } -} - -export abstract class BaseSwitchWindow extends Action { - - private closeWindowAction: IQuickInputButton = { - iconClass: 'action-remove-from-recently-opened', - tooltip: nls.localize('close', "Close Window") - }; - - constructor( - id: string, - label: string, - private windowsService: IWindowsService, - private windowService: IWindowService, - private quickInputService: IQuickInputService, - private keybindingService: IKeybindingService, - private modelService: IModelService, - private modeService: IModeService, - ) { - super(id, label); - - } - - protected abstract isQuickNavigate(): boolean; - - run(): Promise { - const currentWindowId = this.windowService.getCurrentWindowId(); - - return this.windowsService.getWindows().then(windows => { - const placeHolder = nls.localize('switchWindowPlaceHolder', "Select a window to switch to"); - const picks = windows.map(win => { - const resource = win.filename ? URI.file(win.filename) : win.folderUri ? win.folderUri : win.workspace ? URI.file(win.workspace.configPath) : undefined; - const fileKind = win.filename ? FileKind.FILE : win.workspace ? FileKind.ROOT_FOLDER : win.folderUri ? FileKind.FOLDER : FileKind.FILE; - return { - payload: win.id, - label: win.title, - iconClasses: getIconClasses(this.modelService, this.modeService, resource, fileKind), - description: (currentWindowId === win.id) ? nls.localize('current', "Current Window") : undefined, - buttons: (!this.isQuickNavigate() && currentWindowId !== win.id) ? [this.closeWindowAction] : undefined - } as (IQuickPickItem & { payload: number }); - }); - - const autoFocusIndex = (picks.indexOf(picks.filter(pick => pick.payload === currentWindowId)[0]) + 1) % picks.length; - - return this.quickInputService.pick(picks, { - contextKey: 'inWindowsPicker', - activeItem: picks[autoFocusIndex], - placeHolder, - quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined, - onDidTriggerItemButton: context => { - this.windowsService.closeWindow(context.item.payload).then(() => { - context.removeItem(); - }); - } - }); - }).then(pick => { - if (pick) { - this.windowsService.showWindow(pick.payload); - } - }); - } -} - -export class SwitchWindow extends BaseSwitchWindow { - - static readonly ID = 'workbench.action.switchWindow'; - static LABEL = nls.localize('switchWindow', "Switch Window..."); - - constructor( - id: string, - label: string, - @IWindowsService windowsService: IWindowsService, - @IWindowService windowService: IWindowService, - @IQuickInputService quickInputService: IQuickInputService, - @IKeybindingService keybindingService: IKeybindingService, - @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - ) { - super(id, label, windowsService, windowService, quickInputService, keybindingService, modelService, modeService); - } - - protected isQuickNavigate(): boolean { - return false; - } -} - -export class QuickSwitchWindow extends BaseSwitchWindow { - - static readonly ID = 'workbench.action.quickSwitchWindow'; - static LABEL = nls.localize('quickSwitchWindow', "Quick Switch Window..."); - - constructor( - id: string, - label: string, - @IWindowsService windowsService: IWindowsService, - @IWindowService windowService: IWindowService, - @IQuickInputService quickInputService: IQuickInputService, - @IKeybindingService keybindingService: IKeybindingService, - @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - ) { - super(id, label, windowsService, windowService, quickInputService, keybindingService, modelService, modeService); - } - - protected isQuickNavigate(): boolean { - return true; - } -} - -export const inRecentFilesPickerContextKey = 'inRecentFilesPicker'; - -export abstract class BaseOpenRecentAction extends Action { - - private removeFromRecentlyOpened: IQuickInputButton = { - iconClass: 'action-remove-from-recently-opened', - tooltip: nls.localize('remove', "Remove from Recently Opened") - }; - - constructor( - id: string, - label: string, - private windowService: IWindowService, - private windowsService: IWindowsService, - private quickInputService: IQuickInputService, - private contextService: IWorkspaceContextService, - private labelService: ILabelService, - private keybindingService: IKeybindingService, - private modelService: IModelService, - private modeService: IModeService, - ) { - super(id, label); - } - - protected abstract isQuickNavigate(): boolean; - - run(): Promise { - return this.windowService.getRecentlyOpened() - .then(({ workspaces, files }) => this.openRecent(workspaces, files)); - } - - private openRecent(recentWorkspaces: Array, recentFiles: URI[]): void { - - const toPick = (workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI, fileKind: FileKind, labelService: ILabelService, buttons: IQuickInputButton[] | undefined) => { - let resource: URI; - let label: string; - let description: string; - if (isSingleFolderWorkspaceIdentifier(workspace) && fileKind !== FileKind.FILE) { - resource = workspace; - label = labelService.getWorkspaceLabel(workspace); - description = labelService.getUriLabel(dirname(resource)!); - } else if (isWorkspaceIdentifier(workspace)) { - resource = URI.file(workspace.configPath); - label = labelService.getWorkspaceLabel(workspace); - description = labelService.getUriLabel(dirname(resource)!); - } else { - resource = workspace; - label = getBaseLabel(workspace); - description = labelService.getUriLabel(dirname(resource)!); - } - - return { - iconClasses: getIconClasses(this.modelService, this.modeService, resource, fileKind), - label, - description, - buttons, - workspace, - resource, - fileKind, - }; - }; - - const runPick = (resource: URI, isFile: boolean, keyMods: IKeyMods) => { - const forceNewWindow = keyMods.ctrlCmd; - return this.windowService.openWindow([resource], { forceNewWindow, forceOpenWorkspaceAsFile: isFile }); - }; - - const workspacePicks = recentWorkspaces.map(workspace => toPick(workspace, isSingleFolderWorkspaceIdentifier(workspace) ? FileKind.FOLDER : FileKind.ROOT_FOLDER, this.labelService, !this.isQuickNavigate() ? [this.removeFromRecentlyOpened] : undefined)); - const filePicks = recentFiles.map(p => toPick(p, FileKind.FILE, this.labelService, !this.isQuickNavigate() ? [this.removeFromRecentlyOpened] : undefined)); - - // focus second entry if the first recent workspace is the current workspace - let autoFocusSecondEntry: boolean = recentWorkspaces[0] && this.contextService.isCurrentWorkspace(recentWorkspaces[0]); - - let keyMods: IKeyMods; - const workspaceSeparator: IQuickPickSeparator = { type: 'separator', label: nls.localize('workspaces', "workspaces") }; - const fileSeparator: IQuickPickSeparator = { type: 'separator', label: nls.localize('files', "files") }; - const picks = [workspaceSeparator, ...workspacePicks, fileSeparator, ...filePicks]; - this.quickInputService.pick(picks, { - contextKey: inRecentFilesPickerContextKey, - activeItem: [...workspacePicks, ...filePicks][autoFocusSecondEntry ? 1 : 0], - placeHolder: isMacintosh ? nls.localize('openRecentPlaceHolderMac', "Select to open (hold Cmd-key to open in new window)") : nls.localize('openRecentPlaceHolder', "Select to open (hold Ctrl-key to open in new window)"), - matchOnDescription: true, - onKeyMods: mods => keyMods = mods, - quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined, - onDidTriggerItemButton: context => { - this.windowsService.removeFromRecentlyOpened([context.item.workspace]).then(() => context.removeItem()); - } - }) - .then((pick): Promise | void => { - if (pick) { - return runPick(pick.resource, pick.fileKind === FileKind.FILE, keyMods); - } - }); - } -} - -export class OpenRecentAction extends BaseOpenRecentAction { - - static readonly ID = 'workbench.action.openRecent'; - static readonly LABEL = nls.localize('openRecent', "Open Recent..."); - - constructor( - id: string, - label: string, - @IWindowService windowService: IWindowService, - @IWindowsService windowsService: IWindowsService, - @IQuickInputService quickInputService: IQuickInputService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IKeybindingService keybindingService: IKeybindingService, - @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - @ILabelService labelService: ILabelService - ) { - super(id, label, windowService, windowsService, quickInputService, contextService, labelService, keybindingService, modelService, modeService); - } - - protected isQuickNavigate(): boolean { - return false; - } -} - -export class QuickOpenRecentAction extends BaseOpenRecentAction { - - static readonly ID = 'workbench.action.quickOpenRecent'; - static readonly LABEL = nls.localize('quickOpenRecent', "Quick Open Recent..."); - - constructor( - id: string, - label: string, - @IWindowService windowService: IWindowService, - @IWindowsService windowsService: IWindowsService, - @IQuickInputService quickInputService: IQuickInputService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IKeybindingService keybindingService: IKeybindingService, - @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - @ILabelService labelService: ILabelService - ) { - super(id, label, windowService, windowsService, quickInputService, contextService, labelService, keybindingService, modelService, modeService); - } - - protected isQuickNavigate(): boolean { - return true; - } -} - -export class OpenIssueReporterAction extends Action { - static readonly ID = 'workbench.action.openIssueReporter'; - static readonly LABEL = nls.localize({ key: 'reportIssueInEnglish', comment: ['Translate this to "Report Issue in English" in all languages please!'] }, "Report Issue"); - - constructor( - id: string, - label: string, - @IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService - ) { - super(id, label); - } - - run(): Promise { - return this.issueService.openReporter() - .then(() => true); - } -} - -export class OpenProcessExplorer extends Action { - static readonly ID = 'workbench.action.openProcessExplorer'; - static readonly LABEL = nls.localize('openProcessExplorer', "Open Process Explorer"); - - constructor( - id: string, - label: string, - @IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService - ) { - super(id, label); - } - - run(): Promise { - return this.issueService.openProcessExplorer() - .then(() => true); - } -} - -export class ReportPerformanceIssueUsingReporterAction extends Action { - static readonly ID = 'workbench.action.reportPerformanceIssueUsingReporter'; - static readonly LABEL = nls.localize('reportPerformanceIssue', "Report Performance Issue"); - - constructor( - id: string, - label: string, - @IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService - ) { - super(id, label); - } - - run(): Promise { - // TODO: Reporter should send timings table as well - return this.issueService.openReporter({ issueType: IssueType.PerformanceIssue }) - .then(() => true); - } -} - - -export class KeybindingsReferenceAction extends Action { - - static readonly ID = 'workbench.action.keybindingsReference'; - static readonly LABEL = nls.localize('keybindingsReference', "Keyboard Shortcuts Reference"); - - private static readonly URL = isLinux ? product.keyboardShortcutsUrlLinux : isMacintosh ? product.keyboardShortcutsUrlMac : product.keyboardShortcutsUrlWin; - static readonly AVAILABLE = !!KeybindingsReferenceAction.URL; - - constructor( - id: string, - label: string - ) { - super(id, label); - } - - run(): Promise { - window.open(KeybindingsReferenceAction.URL); - return Promise.resolve(); - } -} - -export class OpenDocumentationUrlAction extends Action { - - static readonly ID = 'workbench.action.openDocumentationUrl'; - static readonly LABEL = nls.localize('openDocumentationUrl', "Documentation"); - - private static readonly URL = product.documentationUrl; - static readonly AVAILABLE = !!OpenDocumentationUrlAction.URL; - - constructor( - id: string, - label: string - ) { - super(id, label); - } - - run(): Promise { - window.open(OpenDocumentationUrlAction.URL); - return Promise.resolve(); - } -} - -export class OpenIntroductoryVideosUrlAction extends Action { - - static readonly ID = 'workbench.action.openIntroductoryVideosUrl'; - static readonly LABEL = nls.localize('openIntroductoryVideosUrl', "Introductory Videos"); - - private static readonly URL = product.introductoryVideosUrl; - static readonly AVAILABLE = !!OpenIntroductoryVideosUrlAction.URL; - - constructor( - id: string, - label: string - ) { - super(id, label); - } - - run(): Promise { - window.open(OpenIntroductoryVideosUrlAction.URL); - return Promise.resolve(); - } -} - -export class OpenTipsAndTricksUrlAction extends Action { - - static readonly ID = 'workbench.action.openTipsAndTricksUrl'; - static readonly LABEL = nls.localize('openTipsAndTricksUrl', "Tips and Tricks"); - - private static readonly URL = product.tipsAndTricksUrl; - static readonly AVAILABLE = !!OpenTipsAndTricksUrlAction.URL; - - constructor( - id: string, - label: string - ) { - super(id, label); - } - - run(): Promise { - window.open(OpenTipsAndTricksUrlAction.URL); - return Promise.resolve(); - } -} - -export class ToggleSharedProcessAction extends Action { - - static readonly ID = 'workbench.action.toggleSharedProcess'; - static LABEL = nls.localize('toggleSharedProcess', "Toggle Shared Process"); - - constructor(id: string, label: string, @IWindowsService private readonly windowsService: IWindowsService) { - super(id, label); - } - - run(): Promise { - return this.windowsService.toggleSharedProcess(); - } -} - -export const enum Direction { - Next, - Previous, -} - -export abstract class BaseNavigationAction extends Action { - - constructor( - id: string, - label: string, - @IEditorGroupsService protected editorGroupService: IEditorGroupsService, - @IPanelService protected panelService: IPanelService, - @IPartService protected partService: IPartService, - @IViewletService protected viewletService: IViewletService - ) { - super(id, label); - } - - run(): Promise { - const isEditorFocus = this.partService.hasFocus(Parts.EDITOR_PART); - const isPanelFocus = this.partService.hasFocus(Parts.PANEL_PART); - const isSidebarFocus = this.partService.hasFocus(Parts.SIDEBAR_PART); - - const isSidebarPositionLeft = this.partService.getSideBarPosition() === PartPosition.LEFT; - const isPanelPositionDown = this.partService.getPanelPosition() === PartPosition.BOTTOM; - - if (isEditorFocus) { - return this.navigateOnEditorFocus(isSidebarPositionLeft, isPanelPositionDown); - } - - if (isPanelFocus) { - return this.navigateOnPanelFocus(isSidebarPositionLeft, isPanelPositionDown); - } - - if (isSidebarFocus) { - return Promise.resolve(this.navigateOnSidebarFocus(isSidebarPositionLeft, isPanelPositionDown)); - } - - return Promise.resolve(false); - } - - protected navigateOnEditorFocus(_isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): Promise { - return Promise.resolve(true); - } - - protected navigateOnPanelFocus(_isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): Promise { - return Promise.resolve(true); - } - - protected navigateOnSidebarFocus(_isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): boolean | IViewlet { - return true; - } - - protected navigateToPanel(): IPanel | boolean { - if (!this.partService.isVisible(Parts.PANEL_PART)) { - return false; - } - - const activePanelId = this.panelService.getActivePanel().getId(); - - return this.panelService.openPanel(activePanelId, true); - } - - protected navigateToSidebar(): Promise { - if (!this.partService.isVisible(Parts.SIDEBAR_PART)) { - return Promise.resolve(false); - } - - const activeViewletId = this.viewletService.getActiveViewlet().getId(); - - return this.viewletService.openViewlet(activeViewletId, true) - .then(value => value === null ? false : value); - } - - protected navigateAcrossEditorGroup(direction: GroupDirection): boolean { - return this.doNavigateToEditorGroup({ direction }); - } - - protected navigateToEditorGroup(location: GroupLocation): boolean { - return this.doNavigateToEditorGroup({ location }); - } - - private doNavigateToEditorGroup(scope: IFindGroupScope): boolean { - const targetGroup = this.editorGroupService.findGroup(scope, this.editorGroupService.activeGroup); - if (targetGroup) { - targetGroup.focus(); - - return true; - } - - return false; - } -} - -export class NavigateLeftAction extends BaseNavigationAction { - - static readonly ID = 'workbench.action.navigateLeft'; - static readonly LABEL = nls.localize('navigateLeft', "Navigate to the View on the Left"); - - constructor( - id: string, - label: string, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IPanelService panelService: IPanelService, - @IPartService partService: IPartService, - @IViewletService viewletService: IViewletService - ) { - super(id, label, editorGroupService, panelService, partService, viewletService); - } - - protected navigateOnEditorFocus(isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): Promise { - const didNavigate = this.navigateAcrossEditorGroup(GroupDirection.LEFT); - if (didNavigate) { - return Promise.resolve(true); - } - - if (isSidebarPositionLeft) { - return this.navigateToSidebar(); - } - - return Promise.resolve(false); - } - - protected navigateOnPanelFocus(isSidebarPositionLeft: boolean, isPanelPositionDown: boolean): Promise { - if (isPanelPositionDown && isSidebarPositionLeft) { - return this.navigateToSidebar(); - } - - if (!isPanelPositionDown) { - return Promise.resolve(this.navigateToEditorGroup(GroupLocation.LAST)); - } - - return Promise.resolve(false); - } - - protected navigateOnSidebarFocus(isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): boolean { - if (!isSidebarPositionLeft) { - return this.navigateToEditorGroup(GroupLocation.LAST); - } - - return false; - } -} - -export class NavigateRightAction extends BaseNavigationAction { - - static readonly ID = 'workbench.action.navigateRight'; - static readonly LABEL = nls.localize('navigateRight', "Navigate to the View on the Right"); - - constructor( - id: string, - label: string, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IPanelService panelService: IPanelService, - @IPartService partService: IPartService, - @IViewletService viewletService: IViewletService - ) { - super(id, label, editorGroupService, panelService, partService, viewletService); - } - - protected navigateOnEditorFocus(isSidebarPositionLeft: boolean, isPanelPositionDown: boolean): Promise { - const didNavigate = this.navigateAcrossEditorGroup(GroupDirection.RIGHT); - if (didNavigate) { - return Promise.resolve(true); - } - - if (!isPanelPositionDown) { - return Promise.resolve(this.navigateToPanel()); - } - - if (!isSidebarPositionLeft) { - return this.navigateToSidebar(); - } - - return Promise.resolve(false); - } - - protected navigateOnPanelFocus(isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): Promise { - if (!isSidebarPositionLeft) { - return this.navigateToSidebar(); - } - - return Promise.resolve(false); - } - - protected navigateOnSidebarFocus(isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): boolean { - if (isSidebarPositionLeft) { - return this.navigateToEditorGroup(GroupLocation.FIRST); - } - - return false; - } -} - -export class NavigateUpAction extends BaseNavigationAction { - - static readonly ID = 'workbench.action.navigateUp'; - static readonly LABEL = nls.localize('navigateUp', "Navigate to the View Above"); - - constructor( - id: string, - label: string, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IPanelService panelService: IPanelService, - @IPartService partService: IPartService, - @IViewletService viewletService: IViewletService - ) { - super(id, label, editorGroupService, panelService, partService, viewletService); - } - - protected navigateOnEditorFocus(_isSidebarPositionLeft: boolean, _isPanelPositionDown: boolean): Promise { - return Promise.resolve(this.navigateAcrossEditorGroup(GroupDirection.UP)); - } - - protected navigateOnPanelFocus(_isSidebarPositionLeft: boolean, isPanelPositionDown: boolean): Promise { - if (isPanelPositionDown) { - return Promise.resolve(this.navigateToEditorGroup(GroupLocation.LAST)); - } - - return Promise.resolve(false); - } -} - -export class NavigateDownAction extends BaseNavigationAction { - - static readonly ID = 'workbench.action.navigateDown'; - static readonly LABEL = nls.localize('navigateDown', "Navigate to the View Below"); - - constructor( - id: string, - label: string, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IPanelService panelService: IPanelService, - @IPartService partService: IPartService, - @IViewletService viewletService: IViewletService - ) { - super(id, label, editorGroupService, panelService, partService, viewletService); - } - - protected navigateOnEditorFocus(_isSidebarPositionLeft: boolean, isPanelPositionDown: boolean): Promise { - const didNavigate = this.navigateAcrossEditorGroup(GroupDirection.DOWN); - if (didNavigate) { - return Promise.resolve(true); - } - - if (isPanelPositionDown) { - return Promise.resolve(this.navigateToPanel()); - } - - return Promise.resolve(false); - } -} - -// Resize focused view actions -export abstract class BaseResizeViewAction extends Action { - - // This is a media-size percentage - protected static RESIZE_INCREMENT = 6.5; - - constructor( - id: string, - label: string, - @IPartService protected partService: IPartService - ) { - super(id, label); - } - - protected resizePart(sizeChange: number): void { - const isEditorFocus = this.partService.hasFocus(Parts.EDITOR_PART); - const isSidebarFocus = this.partService.hasFocus(Parts.SIDEBAR_PART); - const isPanelFocus = this.partService.hasFocus(Parts.PANEL_PART); - - let part: Parts | undefined; - if (isSidebarFocus) { - part = Parts.SIDEBAR_PART; - } else if (isPanelFocus) { - part = Parts.PANEL_PART; - } else if (isEditorFocus) { - part = Parts.EDITOR_PART; - } - - if (part) { - this.partService.resizePart(part, sizeChange); - } - } -} - -export class IncreaseViewSizeAction extends BaseResizeViewAction { - - static readonly ID = 'workbench.action.increaseViewSize'; - static readonly LABEL = nls.localize('increaseViewSize', "Increase Current View Size"); - - constructor( - id: string, - label: string, - @IPartService partService: IPartService - ) { - super(id, label, partService); - } - - run(): Promise { - this.resizePart(BaseResizeViewAction.RESIZE_INCREMENT); - return Promise.resolve(true); - } -} - -export class DecreaseViewSizeAction extends BaseResizeViewAction { - - static readonly ID = 'workbench.action.decreaseViewSize'; - static readonly LABEL = nls.localize('decreaseViewSize', "Decrease Current View Size"); - - constructor( - id: string, - label: string, - @IPartService partService: IPartService - - ) { - super(id, label, partService); - } - - run(): Promise { - this.resizePart(-BaseResizeViewAction.RESIZE_INCREMENT); - return Promise.resolve(true); - } -} - -export class NewWindowTab extends Action { - - static readonly ID = 'workbench.action.newWindowTab'; - static readonly LABEL = nls.localize('newTab', "New Window Tab"); - - constructor( - _id: string, - _label: string, - @IWindowsService private readonly windowsService: IWindowsService - ) { - super(NewWindowTab.ID, NewWindowTab.LABEL); - } - - run(): Promise { - return this.windowsService.newWindowTab().then(() => true); - } -} - -export class ShowPreviousWindowTab extends Action { - - static readonly ID = 'workbench.action.showPreviousWindowTab'; - static readonly LABEL = nls.localize('showPreviousTab', "Show Previous Window Tab"); - - constructor( - _id: string, - _label: string, - @IWindowsService private readonly windowsService: IWindowsService - ) { - super(ShowPreviousWindowTab.ID, ShowPreviousWindowTab.LABEL); - } - - run(): Promise { - return this.windowsService.showPreviousWindowTab().then(() => true); - } -} - -export class ShowNextWindowTab extends Action { - - static readonly ID = 'workbench.action.showNextWindowTab'; - static readonly LABEL = nls.localize('showNextWindowTab', "Show Next Window Tab"); - - constructor( - _id: string, - _label: string, - @IWindowsService private readonly windowsService: IWindowsService - ) { - super(ShowNextWindowTab.ID, ShowNextWindowTab.LABEL); - } - - run(): Promise { - return this.windowsService.showNextWindowTab().then(() => true); - } -} - -export class MoveWindowTabToNewWindow extends Action { - - static readonly ID = 'workbench.action.moveWindowTabToNewWindow'; - static readonly LABEL = nls.localize('moveWindowTabToNewWindow', "Move Window Tab to New Window"); - - constructor( - _id: string, - _label: string, - @IWindowsService private readonly windowsService: IWindowsService - ) { - super(MoveWindowTabToNewWindow.ID, MoveWindowTabToNewWindow.LABEL); - } - - run(): Promise { - return this.windowsService.moveWindowTabToNewWindow().then(() => true); - } -} - -export class MergeAllWindowTabs extends Action { - - static readonly ID = 'workbench.action.mergeAllWindowTabs'; - static readonly LABEL = nls.localize('mergeAllWindowTabs', "Merge All Windows"); - - constructor( - _id: string, - _label: string, - @IWindowsService private readonly windowsService: IWindowsService - ) { - super(MergeAllWindowTabs.ID, MergeAllWindowTabs.LABEL); - } - - run(): Promise { - return this.windowsService.mergeAllWindowTabs().then(() => true); - } -} - -export class ToggleWindowTabsBar extends Action { - - static readonly ID = 'workbench.action.toggleWindowTabsBar'; - static readonly LABEL = nls.localize('toggleWindowTabsBar', "Toggle Window Tabs Bar"); - - constructor( - _id: string, - _label: string, - @IWindowsService private readonly windowsService: IWindowsService - ) { - super(ToggleWindowTabsBar.ID, ToggleWindowTabsBar.LABEL); - } - - run(): Promise { - return this.windowsService.toggleWindowTabsBar().then(() => true); - } -} - -export class OpenTwitterUrlAction extends Action { - - static readonly ID = 'workbench.action.openTwitterUrl'; - static LABEL = nls.localize('openTwitterUrl', "Join Us on Twitter", product.applicationName); - - constructor( - id: string, - label: string - ) { - super(id, label); - } - - run(): Promise { - if (product.twitterUrl) { - return Promise.resolve(shell.openExternal(product.twitterUrl)); - } - - return Promise.resolve(false); - } -} - -export class OpenRequestFeatureUrlAction extends Action { - - static readonly ID = 'workbench.action.openRequestFeatureUrl'; - static LABEL = nls.localize('openUserVoiceUrl', "Search Feature Requests"); - - constructor( - id: string, - label: string - ) { - super(id, label); - } - - run(): Promise { - if (product.requestFeatureUrl) { - return Promise.resolve(shell.openExternal(product.requestFeatureUrl)); - } - - return Promise.resolve(false); - } -} - -export class OpenLicenseUrlAction extends Action { - - static readonly ID = 'workbench.action.openLicenseUrl'; - static LABEL = nls.localize('openLicenseUrl', "View License"); - - constructor( - id: string, - label: string - ) { - super(id, label); - } - - run(): Promise { - if (product.licenseUrl) { - if (language) { - const queryArgChar = product.licenseUrl.indexOf('?') > 0 ? '&' : '?'; - return Promise.resolve(shell.openExternal(`${product.licenseUrl}${queryArgChar}lang=${language}`)); - } else { - return Promise.resolve(shell.openExternal(product.licenseUrl)); - } - } - - return Promise.resolve(false); - } -} - - -export class OpenPrivacyStatementUrlAction extends Action { - - static readonly ID = 'workbench.action.openPrivacyStatementUrl'; - static LABEL = nls.localize('openPrivacyStatement', "Privacy Statement"); - - constructor( - id: string, - label: string - ) { - super(id, label); - } - - run(): Promise { - if (product.privacyStatementUrl) { - if (language) { - const queryArgChar = product.privacyStatementUrl.indexOf('?') > 0 ? '&' : '?'; - return Promise.resolve(shell.openExternal(`${product.privacyStatementUrl}${queryArgChar}lang=${language}`)); - } else { - return Promise.resolve(shell.openExternal(product.privacyStatementUrl)); - } - } - - - return Promise.resolve(false); - } -} - -export class ShowAboutDialogAction extends Action { - - static readonly ID = 'workbench.action.showAboutDialog'; - static LABEL = nls.localize('about', "About {0}", product.applicationName); - - constructor( - id: string, - label: string, - @IWindowsService private readonly windowsService: IWindowsService - ) { - super(id, label); - } - - run(): Promise { - return this.windowsService.openAboutDialog(); - } -} - -export class InspectContextKeysAction extends Action { - - static readonly ID = 'workbench.action.inspectContextKeys'; - static LABEL = nls.localize('inspect context keys', "Inspect Context Keys"); - - constructor( - id: string, - label: string, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IWindowService private readonly windowService: IWindowService, - ) { - super(id, label); - } - - run(): Promise { - const disposables: IDisposable[] = []; - - const stylesheet = createStyleSheet(); - disposables.push(toDisposable(() => { - if (stylesheet.parentNode) { - stylesheet.parentNode.removeChild(stylesheet); - } - })); - createCSSRule('*', 'cursor: crosshair !important;', stylesheet); - - const hoverFeedback = document.createElement('div'); - document.body.appendChild(hoverFeedback); - disposables.push(toDisposable(() => document.body.removeChild(hoverFeedback))); - - hoverFeedback.style.position = 'absolute'; - hoverFeedback.style.pointerEvents = 'none'; - hoverFeedback.style.backgroundColor = 'rgba(255, 0, 0, 0.5)'; - hoverFeedback.style.zIndex = '1000'; - - const onMouseMove = domEvent(document.body, 'mousemove', true); - disposables.push(onMouseMove(e => { - const target = e.target as HTMLElement; - const position = getDomNodePagePosition(target); - - hoverFeedback.style.top = `${position.top}px`; - hoverFeedback.style.left = `${position.left}px`; - hoverFeedback.style.width = `${position.width}px`; - hoverFeedback.style.height = `${position.height}px`; - })); - - const onMouseDown = Event.once(domEvent(document.body, 'mousedown', true)); - onMouseDown(e => { e.preventDefault(); e.stopPropagation(); }, null, disposables); - - const onMouseUp = Event.once(domEvent(document.body, 'mouseup', true)); - onMouseUp(e => { - e.preventDefault(); - e.stopPropagation(); - - const context = this.contextKeyService.getContext(e.target as HTMLElement) as Context; - console.log(context.collectAllValues()); - this.windowService.openDevTools(); - - dispose(disposables); - }, null, disposables); - - return Promise.resolve(); - } -} - -export class ToggleScreencastModeAction extends Action { - - static readonly ID = 'workbench.action.toggleScreencastMode'; - static LABEL = nls.localize('toggle mouse clicks', "Toggle Screencast Mode"); - - static disposable: IDisposable | undefined; - - constructor(id: string, label: string, @IKeybindingService private readonly keybindingService: IKeybindingService) { - super(id, label); - } - - async run(): Promise { - if (ToggleScreencastModeAction.disposable) { - ToggleScreencastModeAction.disposable.dispose(); - ToggleScreencastModeAction.disposable = undefined; - return; - } - - const mouseMarker = append(document.body, $('div')); - mouseMarker.style.position = 'absolute'; - mouseMarker.style.border = '2px solid red'; - mouseMarker.style.borderRadius = '20px'; - mouseMarker.style.width = '20px'; - mouseMarker.style.height = '20px'; - mouseMarker.style.top = '0'; - mouseMarker.style.left = '0'; - mouseMarker.style.zIndex = '100000'; - mouseMarker.style.content = ' '; - mouseMarker.style.pointerEvents = 'none'; - mouseMarker.style.display = 'none'; - - const onMouseDown = domEvent(document.body, 'mousedown', true); - const onMouseUp = domEvent(document.body, 'mouseup', true); - const onMouseMove = domEvent(document.body, 'mousemove', true); - - const mouseListener = onMouseDown(e => { - mouseMarker.style.top = `${e.clientY - 10}px`; - mouseMarker.style.left = `${e.clientX - 10}px`; - mouseMarker.style.display = 'block'; - - const mouseMoveListener = onMouseMove(e => { - mouseMarker.style.top = `${e.clientY - 10}px`; - mouseMarker.style.left = `${e.clientX - 10}px`; - }); - - Event.once(onMouseUp)(() => { - mouseMarker.style.display = 'none'; - mouseMoveListener.dispose(); - }); - }); - - const keyboardMarker = append(document.body, $('div')); - keyboardMarker.style.position = 'absolute'; - keyboardMarker.style.backgroundColor = 'rgba(0, 0, 0 ,0.5)'; - keyboardMarker.style.width = '100%'; - keyboardMarker.style.height = '100px'; - keyboardMarker.style.bottom = '20%'; - keyboardMarker.style.left = '0'; - keyboardMarker.style.zIndex = '100000'; - keyboardMarker.style.pointerEvents = 'none'; - keyboardMarker.style.color = 'white'; - keyboardMarker.style.lineHeight = '100px'; - keyboardMarker.style.textAlign = 'center'; - keyboardMarker.style.fontSize = '56px'; - keyboardMarker.style.display = 'none'; - - const onKeyDown = domEvent(document.body, 'keydown', true); - let keyboardTimeout: IDisposable = Disposable.None; - - const keyboardListener = onKeyDown(e => { - keyboardTimeout.dispose(); - - const event = new StandardKeyboardEvent(e); - const keybinding = this.keybindingService.resolveKeyboardEvent(event); - const label = keybinding.getLabel(); - - if (!event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && this.keybindingService.mightProducePrintableCharacter(event) && label) { - keyboardMarker.textContent += ' ' + label; - } else { - keyboardMarker.textContent = label; - } - - keyboardMarker.style.display = 'block'; - - const promise = timeout(800); - keyboardTimeout = toDisposable(() => promise.cancel()); - - promise.then(() => { - keyboardMarker.textContent = ''; - keyboardMarker.style.display = 'none'; - }); - }); - - ToggleScreencastModeAction.disposable = toDisposable(() => { - mouseListener.dispose(); - keyboardListener.dispose(); - document.body.removeChild(mouseMarker); - }); - } -} diff --git a/src/vs/workbench/electron-browser/actions/developerActions.ts b/src/vs/workbench/electron-browser/actions/developerActions.ts new file mode 100644 index 00000000000..8b9c5485b63 --- /dev/null +++ b/src/vs/workbench/electron-browser/actions/developerActions.ts @@ -0,0 +1,210 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; +import * as nls from 'vs/nls'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { domEvent } from 'vs/base/browser/event'; +import { Event } from 'vs/base/common/event'; +import { IDisposable, toDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; +import { getDomNodePagePosition, createStyleSheet, createCSSRule, append, $ } from 'vs/base/browser/dom'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { Context } from 'vs/platform/contextkey/browser/contextKeyService'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { timeout } from 'vs/base/common/async'; + +export class ToggleDevToolsAction extends Action { + + static readonly ID = 'workbench.action.toggleDevTools'; + static LABEL = nls.localize('toggleDevTools', "Toggle Developer Tools"); + + constructor(id: string, label: string, @IWindowService private readonly windowsService: IWindowService) { + super(id, label); + } + + run(): Promise { + return this.windowsService.toggleDevTools(); + } +} + +export class ToggleSharedProcessAction extends Action { + + static readonly ID = 'workbench.action.toggleSharedProcess'; + static LABEL = nls.localize('toggleSharedProcess', "Toggle Shared Process"); + + constructor(id: string, label: string, @IWindowsService private readonly windowsService: IWindowsService) { + super(id, label); + } + + run(): Promise { + return this.windowsService.toggleSharedProcess(); + } +} + +export class InspectContextKeysAction extends Action { + + static readonly ID = 'workbench.action.inspectContextKeys'; + static LABEL = nls.localize('inspect context keys', "Inspect Context Keys"); + + constructor( + id: string, + label: string, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IWindowService private readonly windowService: IWindowService, + ) { + super(id, label); + } + + run(): Promise { + const disposables: IDisposable[] = []; + + const stylesheet = createStyleSheet(); + disposables.push(toDisposable(() => { + if (stylesheet.parentNode) { + stylesheet.parentNode.removeChild(stylesheet); + } + })); + createCSSRule('*', 'cursor: crosshair !important;', stylesheet); + + const hoverFeedback = document.createElement('div'); + document.body.appendChild(hoverFeedback); + disposables.push(toDisposable(() => document.body.removeChild(hoverFeedback))); + + hoverFeedback.style.position = 'absolute'; + hoverFeedback.style.pointerEvents = 'none'; + hoverFeedback.style.backgroundColor = 'rgba(255, 0, 0, 0.5)'; + hoverFeedback.style.zIndex = '1000'; + + const onMouseMove = domEvent(document.body, 'mousemove', true); + disposables.push(onMouseMove(e => { + const target = e.target as HTMLElement; + const position = getDomNodePagePosition(target); + + hoverFeedback.style.top = `${position.top}px`; + hoverFeedback.style.left = `${position.left}px`; + hoverFeedback.style.width = `${position.width}px`; + hoverFeedback.style.height = `${position.height}px`; + })); + + const onMouseDown = Event.once(domEvent(document.body, 'mousedown', true)); + onMouseDown(e => { e.preventDefault(); e.stopPropagation(); }, null, disposables); + + const onMouseUp = Event.once(domEvent(document.body, 'mouseup', true)); + onMouseUp(e => { + e.preventDefault(); + e.stopPropagation(); + + const context = this.contextKeyService.getContext(e.target as HTMLElement) as Context; + console.log(context.collectAllValues()); + this.windowService.openDevTools(); + + dispose(disposables); + }, null, disposables); + + return Promise.resolve(); + } +} + +export class ToggleScreencastModeAction extends Action { + + static readonly ID = 'workbench.action.toggleScreencastMode'; + static LABEL = nls.localize('toggle mouse clicks', "Toggle Screencast Mode"); + + static disposable: IDisposable | undefined; + + constructor(id: string, label: string, @IKeybindingService private readonly keybindingService: IKeybindingService) { + super(id, label); + } + + async run(): Promise { + if (ToggleScreencastModeAction.disposable) { + ToggleScreencastModeAction.disposable.dispose(); + ToggleScreencastModeAction.disposable = undefined; + return; + } + + const mouseMarker = append(document.body, $('div')); + mouseMarker.style.position = 'absolute'; + mouseMarker.style.border = '2px solid red'; + mouseMarker.style.borderRadius = '20px'; + mouseMarker.style.width = '20px'; + mouseMarker.style.height = '20px'; + mouseMarker.style.top = '0'; + mouseMarker.style.left = '0'; + mouseMarker.style.zIndex = '100000'; + mouseMarker.style.content = ' '; + mouseMarker.style.pointerEvents = 'none'; + mouseMarker.style.display = 'none'; + + const onMouseDown = domEvent(document.body, 'mousedown', true); + const onMouseUp = domEvent(document.body, 'mouseup', true); + const onMouseMove = domEvent(document.body, 'mousemove', true); + + const mouseListener = onMouseDown(e => { + mouseMarker.style.top = `${e.clientY - 10}px`; + mouseMarker.style.left = `${e.clientX - 10}px`; + mouseMarker.style.display = 'block'; + + const mouseMoveListener = onMouseMove(e => { + mouseMarker.style.top = `${e.clientY - 10}px`; + mouseMarker.style.left = `${e.clientX - 10}px`; + }); + + Event.once(onMouseUp)(() => { + mouseMarker.style.display = 'none'; + mouseMoveListener.dispose(); + }); + }); + + const keyboardMarker = append(document.body, $('div')); + keyboardMarker.style.position = 'absolute'; + keyboardMarker.style.backgroundColor = 'rgba(0, 0, 0 ,0.5)'; + keyboardMarker.style.width = '100%'; + keyboardMarker.style.height = '100px'; + keyboardMarker.style.bottom = '20%'; + keyboardMarker.style.left = '0'; + keyboardMarker.style.zIndex = '100000'; + keyboardMarker.style.pointerEvents = 'none'; + keyboardMarker.style.color = 'white'; + keyboardMarker.style.lineHeight = '100px'; + keyboardMarker.style.textAlign = 'center'; + keyboardMarker.style.fontSize = '56px'; + keyboardMarker.style.display = 'none'; + + const onKeyDown = domEvent(document.body, 'keydown', true); + let keyboardTimeout: IDisposable = Disposable.None; + + const keyboardListener = onKeyDown(e => { + keyboardTimeout.dispose(); + + const event = new StandardKeyboardEvent(e); + const keybinding = this.keybindingService.resolveKeyboardEvent(event); + const label = keybinding.getLabel(); + + if (!event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && this.keybindingService.mightProducePrintableCharacter(event) && label) { + keyboardMarker.textContent += ' ' + label; + } else { + keyboardMarker.textContent = label; + } + + keyboardMarker.style.display = 'block'; + + const promise = timeout(800); + keyboardTimeout = toDisposable(() => promise.cancel()); + + promise.then(() => { + keyboardMarker.textContent = ''; + keyboardMarker.style.display = 'none'; + }); + }); + + ToggleScreencastModeAction.disposable = toDisposable(() => { + mouseListener.dispose(); + keyboardListener.dispose(); + document.body.removeChild(mouseMarker); + }); + } +} diff --git a/src/vs/workbench/electron-browser/actions/helpActions.ts b/src/vs/workbench/electron-browser/actions/helpActions.ts new file mode 100644 index 00000000000..85d39d64d7b --- /dev/null +++ b/src/vs/workbench/electron-browser/actions/helpActions.ts @@ -0,0 +1,244 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as nls from 'vs/nls'; +import product from 'vs/platform/node/product'; +import { isMacintosh, isLinux, language } from 'vs/base/common/platform'; +import { IssueType } from 'vs/platform/issue/common/issue'; +import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; + +export class OpenIssueReporterAction extends Action { + static readonly ID = 'workbench.action.openIssueReporter'; + static readonly LABEL = nls.localize({ key: 'reportIssueInEnglish', comment: ['Translate this to "Report Issue in English" in all languages please!'] }, "Report Issue"); + + constructor( + id: string, + label: string, + @IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService + ) { + super(id, label); + } + + run(): Promise { + return this.issueService.openReporter().then(() => true); + } +} + +export class OpenProcessExplorer extends Action { + static readonly ID = 'workbench.action.openProcessExplorer'; + static readonly LABEL = nls.localize('openProcessExplorer', "Open Process Explorer"); + + constructor( + id: string, + label: string, + @IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService + ) { + super(id, label); + } + + run(): Promise { + return this.issueService.openProcessExplorer().then(() => true); + } +} + +export class ReportPerformanceIssueUsingReporterAction extends Action { + static readonly ID = 'workbench.action.reportPerformanceIssueUsingReporter'; + static readonly LABEL = nls.localize('reportPerformanceIssue', "Report Performance Issue"); + + constructor( + id: string, + label: string, + @IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService + ) { + super(id, label); + } + + run(): Promise { + return this.issueService.openReporter({ issueType: IssueType.PerformanceIssue }).then(() => true); + } +} + +export class KeybindingsReferenceAction extends Action { + + static readonly ID = 'workbench.action.keybindingsReference'; + static readonly LABEL = nls.localize('keybindingsReference', "Keyboard Shortcuts Reference"); + + private static readonly URL = isLinux ? product.keyboardShortcutsUrlLinux : isMacintosh ? product.keyboardShortcutsUrlMac : product.keyboardShortcutsUrlWin; + static readonly AVAILABLE = !!KeybindingsReferenceAction.URL; + + constructor( + id: string, + label: string + ) { + super(id, label); + } + + run(): Promise { + window.open(KeybindingsReferenceAction.URL); + + return Promise.resolve(); + } +} + +export class OpenDocumentationUrlAction extends Action { + + static readonly ID = 'workbench.action.openDocumentationUrl'; + static readonly LABEL = nls.localize('openDocumentationUrl', "Documentation"); + + private static readonly URL = product.documentationUrl; + static readonly AVAILABLE = !!OpenDocumentationUrlAction.URL; + + constructor( + id: string, + label: string + ) { + super(id, label); + } + + run(): Promise { + window.open(OpenDocumentationUrlAction.URL); + + return Promise.resolve(); + } +} + +export class OpenIntroductoryVideosUrlAction extends Action { + + static readonly ID = 'workbench.action.openIntroductoryVideosUrl'; + static readonly LABEL = nls.localize('openIntroductoryVideosUrl', "Introductory Videos"); + + private static readonly URL = product.introductoryVideosUrl; + static readonly AVAILABLE = !!OpenIntroductoryVideosUrlAction.URL; + + constructor( + id: string, + label: string + ) { + super(id, label); + } + + run(): Promise { + window.open(OpenIntroductoryVideosUrlAction.URL); + + return Promise.resolve(); + } +} + +export class OpenTipsAndTricksUrlAction extends Action { + + static readonly ID = 'workbench.action.openTipsAndTricksUrl'; + static readonly LABEL = nls.localize('openTipsAndTricksUrl', "Tips and Tricks"); + + private static readonly URL = product.tipsAndTricksUrl; + static readonly AVAILABLE = !!OpenTipsAndTricksUrlAction.URL; + + constructor( + id: string, + label: string + ) { + super(id, label); + } + + run(): Promise { + window.open(OpenTipsAndTricksUrlAction.URL); + + return Promise.resolve(); + } +} + +export class OpenTwitterUrlAction extends Action { + + static readonly ID = 'workbench.action.openTwitterUrl'; + static LABEL = nls.localize('openTwitterUrl', "Join Us on Twitter", product.applicationName); + + constructor( + id: string, + label: string + ) { + super(id, label); + } + + run(): Promise { + if (product.twitterUrl) { + window.open(product.twitterUrl); + } + + return Promise.resolve(); + } +} + +export class OpenRequestFeatureUrlAction extends Action { + + static readonly ID = 'workbench.action.openRequestFeatureUrl'; + static LABEL = nls.localize('openUserVoiceUrl', "Search Feature Requests"); + + constructor( + id: string, + label: string + ) { + super(id, label); + } + + run(): Promise { + if (product.requestFeatureUrl) { + window.open(product.requestFeatureUrl); + } + + return Promise.resolve(); + } +} + +export class OpenLicenseUrlAction extends Action { + + static readonly ID = 'workbench.action.openLicenseUrl'; + static LABEL = nls.localize('openLicenseUrl', "View License"); + + constructor( + id: string, + label: string + ) { + super(id, label); + } + + run(): Promise { + if (product.licenseUrl) { + if (language) { + const queryArgChar = product.licenseUrl.indexOf('?') > 0 ? '&' : '?'; + window.open(`${product.licenseUrl}${queryArgChar}lang=${language}`); + } else { + window.open(product.licenseUrl); + } + } + + return Promise.resolve(); + } +} + +export class OpenPrivacyStatementUrlAction extends Action { + + static readonly ID = 'workbench.action.openPrivacyStatementUrl'; + static LABEL = nls.localize('openPrivacyStatement', "Privacy Statement"); + + constructor( + id: string, + label: string + ) { + super(id, label); + } + + run(): Promise { + if (product.privacyStatementUrl) { + if (language) { + const queryArgChar = product.privacyStatementUrl.indexOf('?') > 0 ? '&' : '?'; + window.open(`${product.privacyStatementUrl}${queryArgChar}lang=${language}`); + } else { + window.open(product.privacyStatementUrl); + } + } + + return Promise.resolve(); + } +} diff --git a/src/vs/workbench/electron-browser/media/actions.css b/src/vs/workbench/electron-browser/actions/media/actions.css similarity index 100% rename from src/vs/workbench/electron-browser/media/actions.css rename to src/vs/workbench/electron-browser/actions/media/actions.css diff --git a/src/vs/workbench/electron-browser/media/remove-dark.svg b/src/vs/workbench/electron-browser/actions/media/remove-dark.svg similarity index 100% rename from src/vs/workbench/electron-browser/media/remove-dark.svg rename to src/vs/workbench/electron-browser/actions/media/remove-dark.svg diff --git a/src/vs/workbench/electron-browser/media/remove.svg b/src/vs/workbench/electron-browser/actions/media/remove.svg similarity index 100% rename from src/vs/workbench/electron-browser/media/remove.svg rename to src/vs/workbench/electron-browser/actions/media/remove.svg diff --git a/src/vs/workbench/electron-browser/actions/windowActions.ts b/src/vs/workbench/electron-browser/actions/windowActions.ts new file mode 100644 index 00000000000..0cf3ec66110 --- /dev/null +++ b/src/vs/workbench/electron-browser/actions/windowActions.ts @@ -0,0 +1,577 @@ +/*--------------------------------------------------------------------------------------------- + * 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/actions'; + +import { URI } from 'vs/base/common/uri'; +import { Action } from 'vs/base/common/actions'; +import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; +import * as nls from 'vs/nls'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; +import { isMacintosh } from 'vs/base/common/platform'; +import * as browser from 'vs/base/browser/browser'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { webFrame } from 'electron'; +import { getBaseLabel } from 'vs/base/common/labels'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { FileKind } from 'vs/platform/files/common/files'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { dirname } from 'vs/base/common/resources'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { IQuickInputService, IQuickPickItem, IQuickInputButton, IQuickPickSeparator, IKeyMods } from 'vs/platform/quickinput/common/quickInput'; +import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; +import product from 'vs/platform/node/product'; + +export class CloseCurrentWindowAction extends Action { + + static readonly ID = 'workbench.action.closeWindow'; + static readonly LABEL = nls.localize('closeWindow', "Close Window"); + + constructor(id: string, label: string, @IWindowService private readonly windowService: IWindowService) { + super(id, label); + } + + run(): Promise { + this.windowService.closeWindow(); + + return Promise.resolve(true); + } +} + +export class NewWindowAction extends Action { + + static readonly ID = 'workbench.action.newWindow'; + static LABEL = nls.localize('newWindow', "New Window"); + + constructor( + id: string, + label: string, + @IWindowsService private readonly windowsService: IWindowsService + ) { + super(id, label); + } + + run(): Promise { + return this.windowsService.openNewWindow(); + } +} + +export class ToggleFullScreenAction extends Action { + + static readonly ID = 'workbench.action.toggleFullScreen'; + static LABEL = nls.localize('toggleFullScreen', "Toggle Full Screen"); + + constructor(id: string, label: string, @IWindowService private readonly windowService: IWindowService) { + super(id, label); + } + + run(): Promise { + return this.windowService.toggleFullScreen(); + } +} + +export abstract class BaseZoomAction extends Action { + private static readonly SETTING_KEY = 'window.zoomLevel'; + + constructor( + id: string, + label: string, + @IWorkspaceConfigurationService private readonly configurationService: IWorkspaceConfigurationService + ) { + super(id, label); + } + + protected setConfiguredZoomLevel(level: number): void { + level = Math.round(level); // when reaching smallest zoom, prevent fractional zoom levels + + const applyZoom = () => { + webFrame.setZoomLevel(level); + browser.setZoomFactor(webFrame.getZoomFactor()); + // See https://github.com/Microsoft/vscode/issues/26151 + // Cannot be trusted because the webFrame might take some time + // until it really applies the new zoom level + browser.setZoomLevel(webFrame.getZoomLevel(), /*isTrusted*/false); + }; + + this.configurationService.updateValue(BaseZoomAction.SETTING_KEY, level).then(() => applyZoom()); + } +} + +export class ZoomInAction extends BaseZoomAction { + + static readonly ID = 'workbench.action.zoomIn'; + static readonly LABEL = nls.localize('zoomIn', "Zoom In"); + + constructor( + id: string, + label: string, + @IWorkspaceConfigurationService configurationService: IWorkspaceConfigurationService + ) { + super(id, label, configurationService); + } + + run(): Promise { + this.setConfiguredZoomLevel(webFrame.getZoomLevel() + 1); + + return Promise.resolve(true); + } +} + +export class ZoomOutAction extends BaseZoomAction { + + static readonly ID = 'workbench.action.zoomOut'; + static readonly LABEL = nls.localize('zoomOut', "Zoom Out"); + + constructor( + id: string, + label: string, + @IWorkspaceConfigurationService configurationService: IWorkspaceConfigurationService + ) { + super(id, label, configurationService); + } + + run(): Promise { + this.setConfiguredZoomLevel(webFrame.getZoomLevel() - 1); + + return Promise.resolve(true); + } +} + +export class ZoomResetAction extends BaseZoomAction { + + static readonly ID = 'workbench.action.zoomReset'; + static readonly LABEL = nls.localize('zoomReset', "Reset Zoom"); + + constructor( + id: string, + label: string, + @IWorkspaceConfigurationService configurationService: IWorkspaceConfigurationService + ) { + super(id, label, configurationService); + } + + run(): Promise { + this.setConfiguredZoomLevel(0); + + return Promise.resolve(true); + } +} + +export class ReloadWindowAction extends Action { + + static readonly ID = 'workbench.action.reloadWindow'; + static LABEL = nls.localize('reloadWindow', "Reload Window"); + + constructor( + id: string, + label: string, + @IWindowService private readonly windowService: IWindowService + ) { + super(id, label); + } + + run(): Promise { + return this.windowService.reloadWindow().then(() => true); + } +} + +export class ReloadWindowWithExtensionsDisabledAction extends Action { + + static readonly ID = 'workbench.action.reloadWindowWithExtensionsDisabled'; + static LABEL = nls.localize('reloadWindowWithExntesionsDisabled', "Reload Window With Extensions Disabled"); + + constructor( + id: string, + label: string, + @IWindowService private readonly windowService: IWindowService + ) { + super(id, label); + } + + run(): Promise { + return this.windowService.reloadWindow({ _: [], 'disable-extensions': true }).then(() => true); + } +} + +export abstract class BaseSwitchWindow extends Action { + + private closeWindowAction: IQuickInputButton = { + iconClass: 'action-remove-from-recently-opened', + tooltip: nls.localize('close', "Close Window") + }; + + constructor( + id: string, + label: string, + private windowsService: IWindowsService, + private windowService: IWindowService, + private quickInputService: IQuickInputService, + private keybindingService: IKeybindingService, + private modelService: IModelService, + private modeService: IModeService, + ) { + super(id, label); + + } + + protected abstract isQuickNavigate(): boolean; + + run(): Promise { + const currentWindowId = this.windowService.getCurrentWindowId(); + + return this.windowsService.getWindows().then(windows => { + const placeHolder = nls.localize('switchWindowPlaceHolder', "Select a window to switch to"); + const picks = windows.map(win => { + const resource = win.filename ? URI.file(win.filename) : win.folderUri ? win.folderUri : win.workspace ? URI.file(win.workspace.configPath) : undefined; + const fileKind = win.filename ? FileKind.FILE : win.workspace ? FileKind.ROOT_FOLDER : win.folderUri ? FileKind.FOLDER : FileKind.FILE; + return { + payload: win.id, + label: win.title, + iconClasses: getIconClasses(this.modelService, this.modeService, resource, fileKind), + description: (currentWindowId === win.id) ? nls.localize('current', "Current Window") : undefined, + buttons: (!this.isQuickNavigate() && currentWindowId !== win.id) ? [this.closeWindowAction] : undefined + } as (IQuickPickItem & { payload: number }); + }); + + const autoFocusIndex = (picks.indexOf(picks.filter(pick => pick.payload === currentWindowId)[0]) + 1) % picks.length; + + return this.quickInputService.pick(picks, { + contextKey: 'inWindowsPicker', + activeItem: picks[autoFocusIndex], + placeHolder, + quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined, + onDidTriggerItemButton: context => { + this.windowsService.closeWindow(context.item.payload).then(() => { + context.removeItem(); + }); + } + }); + }).then(pick => { + if (pick) { + this.windowsService.showWindow(pick.payload); + } + }); + } +} + +export class SwitchWindow extends BaseSwitchWindow { + + static readonly ID = 'workbench.action.switchWindow'; + static LABEL = nls.localize('switchWindow', "Switch Window..."); + + constructor( + id: string, + label: string, + @IWindowsService windowsService: IWindowsService, + @IWindowService windowService: IWindowService, + @IQuickInputService quickInputService: IQuickInputService, + @IKeybindingService keybindingService: IKeybindingService, + @IModelService modelService: IModelService, + @IModeService modeService: IModeService, + ) { + super(id, label, windowsService, windowService, quickInputService, keybindingService, modelService, modeService); + } + + protected isQuickNavigate(): boolean { + return false; + } +} + +export class QuickSwitchWindow extends BaseSwitchWindow { + + static readonly ID = 'workbench.action.quickSwitchWindow'; + static LABEL = nls.localize('quickSwitchWindow', "Quick Switch Window..."); + + constructor( + id: string, + label: string, + @IWindowsService windowsService: IWindowsService, + @IWindowService windowService: IWindowService, + @IQuickInputService quickInputService: IQuickInputService, + @IKeybindingService keybindingService: IKeybindingService, + @IModelService modelService: IModelService, + @IModeService modeService: IModeService, + ) { + super(id, label, windowsService, windowService, quickInputService, keybindingService, modelService, modeService); + } + + protected isQuickNavigate(): boolean { + return true; + } +} + +export const inRecentFilesPickerContextKey = 'inRecentFilesPicker'; + +export abstract class BaseOpenRecentAction extends Action { + + private removeFromRecentlyOpened: IQuickInputButton = { + iconClass: 'action-remove-from-recently-opened', + tooltip: nls.localize('remove', "Remove from Recently Opened") + }; + + constructor( + id: string, + label: string, + private windowService: IWindowService, + private windowsService: IWindowsService, + private quickInputService: IQuickInputService, + private contextService: IWorkspaceContextService, + private labelService: ILabelService, + private keybindingService: IKeybindingService, + private modelService: IModelService, + private modeService: IModeService, + ) { + super(id, label); + } + + protected abstract isQuickNavigate(): boolean; + + run(): Promise { + return this.windowService.getRecentlyOpened() + .then(({ workspaces, files }) => this.openRecent(workspaces, files)); + } + + private openRecent(recentWorkspaces: Array, recentFiles: URI[]): void { + + const toPick = (workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI, fileKind: FileKind, labelService: ILabelService, buttons: IQuickInputButton[] | undefined) => { + let resource: URI; + let label: string; + let description: string; + if (isSingleFolderWorkspaceIdentifier(workspace) && fileKind !== FileKind.FILE) { + resource = workspace; + label = labelService.getWorkspaceLabel(workspace); + description = labelService.getUriLabel(dirname(resource)!); + } else if (isWorkspaceIdentifier(workspace)) { + resource = URI.file(workspace.configPath); + label = labelService.getWorkspaceLabel(workspace); + description = labelService.getUriLabel(dirname(resource)!); + } else { + resource = workspace; + label = getBaseLabel(workspace); + description = labelService.getUriLabel(dirname(resource)!); + } + + return { + iconClasses: getIconClasses(this.modelService, this.modeService, resource, fileKind), + label, + description, + buttons, + workspace, + resource, + fileKind, + }; + }; + + const runPick = (resource: URI, isFile: boolean, keyMods: IKeyMods) => { + const forceNewWindow = keyMods.ctrlCmd; + return this.windowService.openWindow([resource], { forceNewWindow, forceOpenWorkspaceAsFile: isFile }); + }; + + const workspacePicks = recentWorkspaces.map(workspace => toPick(workspace, isSingleFolderWorkspaceIdentifier(workspace) ? FileKind.FOLDER : FileKind.ROOT_FOLDER, this.labelService, !this.isQuickNavigate() ? [this.removeFromRecentlyOpened] : undefined)); + const filePicks = recentFiles.map(p => toPick(p, FileKind.FILE, this.labelService, !this.isQuickNavigate() ? [this.removeFromRecentlyOpened] : undefined)); + + // focus second entry if the first recent workspace is the current workspace + let autoFocusSecondEntry: boolean = recentWorkspaces[0] && this.contextService.isCurrentWorkspace(recentWorkspaces[0]); + + let keyMods: IKeyMods; + const workspaceSeparator: IQuickPickSeparator = { type: 'separator', label: nls.localize('workspaces', "workspaces") }; + const fileSeparator: IQuickPickSeparator = { type: 'separator', label: nls.localize('files', "files") }; + const picks = [workspaceSeparator, ...workspacePicks, fileSeparator, ...filePicks]; + this.quickInputService.pick(picks, { + contextKey: inRecentFilesPickerContextKey, + activeItem: [...workspacePicks, ...filePicks][autoFocusSecondEntry ? 1 : 0], + placeHolder: isMacintosh ? nls.localize('openRecentPlaceHolderMac', "Select to open (hold Cmd-key to open in new window)") : nls.localize('openRecentPlaceHolder', "Select to open (hold Ctrl-key to open in new window)"), + matchOnDescription: true, + onKeyMods: mods => keyMods = mods, + quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined, + onDidTriggerItemButton: context => { + this.windowsService.removeFromRecentlyOpened([context.item.workspace]).then(() => context.removeItem()); + } + }) + .then((pick): Promise | void => { + if (pick) { + return runPick(pick.resource, pick.fileKind === FileKind.FILE, keyMods); + } + }); + } +} + +export class OpenRecentAction extends BaseOpenRecentAction { + + static readonly ID = 'workbench.action.openRecent'; + static readonly LABEL = nls.localize('openRecent', "Open Recent..."); + + constructor( + id: string, + label: string, + @IWindowService windowService: IWindowService, + @IWindowsService windowsService: IWindowsService, + @IQuickInputService quickInputService: IQuickInputService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IKeybindingService keybindingService: IKeybindingService, + @IModelService modelService: IModelService, + @IModeService modeService: IModeService, + @ILabelService labelService: ILabelService + ) { + super(id, label, windowService, windowsService, quickInputService, contextService, labelService, keybindingService, modelService, modeService); + } + + protected isQuickNavigate(): boolean { + return false; + } +} + +export class QuickOpenRecentAction extends BaseOpenRecentAction { + + static readonly ID = 'workbench.action.quickOpenRecent'; + static readonly LABEL = nls.localize('quickOpenRecent', "Quick Open Recent..."); + + constructor( + id: string, + label: string, + @IWindowService windowService: IWindowService, + @IWindowsService windowsService: IWindowsService, + @IQuickInputService quickInputService: IQuickInputService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IKeybindingService keybindingService: IKeybindingService, + @IModelService modelService: IModelService, + @IModeService modeService: IModeService, + @ILabelService labelService: ILabelService + ) { + super(id, label, windowService, windowsService, quickInputService, contextService, labelService, keybindingService, modelService, modeService); + } + + protected isQuickNavigate(): boolean { + return true; + } +} + +export class ShowAboutDialogAction extends Action { + + static readonly ID = 'workbench.action.showAboutDialog'; + static LABEL = nls.localize('about', "About {0}", product.applicationName); + + constructor( + id: string, + label: string, + @IWindowsService private readonly windowsService: IWindowsService + ) { + super(id, label); + } + + run(): Promise { + return this.windowsService.openAboutDialog(); + } +} + +export class NewWindowTab extends Action { + + static readonly ID = 'workbench.action.newWindowTab'; + static readonly LABEL = nls.localize('newTab', "New Window Tab"); + + constructor( + _id: string, + _label: string, + @IWindowsService private readonly windowsService: IWindowsService + ) { + super(NewWindowTab.ID, NewWindowTab.LABEL); + } + + run(): Promise { + return this.windowsService.newWindowTab().then(() => true); + } +} + +export class ShowPreviousWindowTab extends Action { + + static readonly ID = 'workbench.action.showPreviousWindowTab'; + static readonly LABEL = nls.localize('showPreviousTab', "Show Previous Window Tab"); + + constructor( + _id: string, + _label: string, + @IWindowsService private readonly windowsService: IWindowsService + ) { + super(ShowPreviousWindowTab.ID, ShowPreviousWindowTab.LABEL); + } + + run(): Promise { + return this.windowsService.showPreviousWindowTab().then(() => true); + } +} + +export class ShowNextWindowTab extends Action { + + static readonly ID = 'workbench.action.showNextWindowTab'; + static readonly LABEL = nls.localize('showNextWindowTab', "Show Next Window Tab"); + + constructor( + _id: string, + _label: string, + @IWindowsService private readonly windowsService: IWindowsService + ) { + super(ShowNextWindowTab.ID, ShowNextWindowTab.LABEL); + } + + run(): Promise { + return this.windowsService.showNextWindowTab().then(() => true); + } +} + +export class MoveWindowTabToNewWindow extends Action { + + static readonly ID = 'workbench.action.moveWindowTabToNewWindow'; + static readonly LABEL = nls.localize('moveWindowTabToNewWindow', "Move Window Tab to New Window"); + + constructor( + _id: string, + _label: string, + @IWindowsService private readonly windowsService: IWindowsService + ) { + super(MoveWindowTabToNewWindow.ID, MoveWindowTabToNewWindow.LABEL); + } + + run(): Promise { + return this.windowsService.moveWindowTabToNewWindow().then(() => true); + } +} + +export class MergeAllWindowTabs extends Action { + + static readonly ID = 'workbench.action.mergeAllWindowTabs'; + static readonly LABEL = nls.localize('mergeAllWindowTabs', "Merge All Windows"); + + constructor( + _id: string, + _label: string, + @IWindowsService private readonly windowsService: IWindowsService + ) { + super(MergeAllWindowTabs.ID, MergeAllWindowTabs.LABEL); + } + + run(): Promise { + return this.windowsService.mergeAllWindowTabs().then(() => true); + } +} + +export class ToggleWindowTabsBar extends Action { + + static readonly ID = 'workbench.action.toggleWindowTabsBar'; + static readonly LABEL = nls.localize('toggleWindowTabsBar', "Toggle Window Tabs Bar"); + + constructor( + _id: string, + _label: string, + @IWindowsService private readonly windowsService: IWindowsService + ) { + super(ToggleWindowTabsBar.ID, ToggleWindowTabsBar.LABEL); + } + + run(): Promise { + return this.windowsService.toggleWindowTabsBar().then(() => true); + } +} diff --git a/src/vs/workbench/electron-browser/commands.ts b/src/vs/workbench/electron-browser/commands.ts deleted file mode 100644 index fcc3b93ce06..00000000000 --- a/src/vs/workbench/electron-browser/commands.ts +++ /dev/null @@ -1,776 +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 { tmpdir } from 'os'; -import { posix } from 'path'; -import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IPartService } from 'vs/workbench/services/part/common/partService'; -import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows'; -import { List } from 'vs/base/browser/ui/list/listWidget'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { WorkbenchListFocusContextKey, IListService, WorkbenchListSupportsMultiSelectContextKey, ListWidget, WorkbenchListHasSelectionOrFocus } from 'vs/platform/list/browser/listService'; -import { PagedList } from 'vs/base/browser/ui/list/listPaging'; -import { range } from 'vs/base/common/arrays'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { ITree } from 'vs/base/parts/tree/browser/tree'; -import { InEditorZenModeContext, NoEditorsVisibleContext, SingleEditorGroupsContext } from 'vs/workbench/common/editor'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { URI } from 'vs/base/common/uri'; -import { IDownloadService } from 'vs/platform/download/common/download'; -import { generateUuid } from 'vs/base/common/uuid'; -import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; -import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; -import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; - -// --- List Commands - -function ensureDOMFocus(widget: ListWidget): void { - // it can happen that one of the commands is executed while - // DOM focus is within another focusable control within the - // list/tree item. therefor we should ensure that the - // list/tree has DOM focus again after the command ran. - if (widget && widget.getHTMLElement() !== document.activeElement) { - widget.domFocus(); - } -} - -export const QUIT_ID = 'workbench.action.quit'; -export function registerCommands(): void { - - function focusDown(accessor: ServicesAccessor, arg2?: number): void { - const focused = accessor.get(IListService).lastFocusedList; - const count = typeof arg2 === 'number' ? arg2 : 1; - - // Ensure DOM Focus - ensureDOMFocus(focused); - - // List - if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - - list.focusNext(count); - const listFocus = list.getFocus(); - if (listFocus.length) { - list.reveal(listFocus[0]); - } - } - - // ObjectTree - else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const tree = focused; - - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - tree.focusNext(count, true, fakeKeyboardEvent); - - const listFocus = tree.getFocus(); - if (listFocus.length) { - tree.reveal(listFocus[0]); - } - } - - // Tree - else if (focused) { - const tree = focused; - - tree.focusNext(count, { origin: 'keyboard' }); - tree.reveal(tree.getFocus()); - } - } - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.focusDown', - weight: KeybindingWeight.WorkbenchContrib, - when: WorkbenchListFocusContextKey, - primary: KeyCode.DownArrow, - mac: { - primary: KeyCode.DownArrow, - secondary: [KeyMod.WinCtrl | KeyCode.KEY_N] - }, - handler: (accessor, arg2) => focusDown(accessor, arg2) - }); - - function expandMultiSelection(focused: List | PagedList | ITree | ObjectTree | DataTree | AsyncDataTree, previousFocus: any): void { - - // List - if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - - const focus = list.getFocus() ? list.getFocus()[0] : undefined; - const selection = list.getSelection(); - if (selection && selection.indexOf(focus) >= 0) { - list.setSelection(selection.filter(s => s !== previousFocus)); - } else { - list.setSelection(selection.concat(focus)); - } - } - - // ObjectTree - else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const list = focused; - - const focus = list.getFocus() ? list.getFocus()[0] : undefined; - const selection = list.getSelection(); - const fakeKeyboardEvent = new KeyboardEvent('keydown', { shiftKey: true }); - - if (selection && selection.indexOf(focus) >= 0) { - list.setSelection(selection.filter(s => s !== previousFocus), fakeKeyboardEvent); - } else { - list.setSelection(selection.concat(focus), fakeKeyboardEvent); - } - } - - // Tree - else if (focused) { - const tree = focused; - - const focus = tree.getFocus(); - const selection = tree.getSelection(); - if (selection && selection.indexOf(focus) >= 0) { - tree.setSelection(selection.filter(s => s !== previousFocus)); - } else { - tree.setSelection(selection.concat(focus)); - } - } - } - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.expandSelectionDown', - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListSupportsMultiSelectContextKey), - primary: KeyMod.Shift | KeyCode.DownArrow, - handler: (accessor, arg2) => { - const focused = accessor.get(IListService).lastFocusedList; - - // List - if (focused instanceof List || focused instanceof PagedList || focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const list = focused; - - // Focus down first - const previousFocus = list.getFocus() ? list.getFocus()[0] : undefined; - focusDown(accessor, arg2); - - // Then adjust selection - expandMultiSelection(focused, previousFocus); - } - - // Tree - else if (focused) { - const tree = focused; - - // Focus down first - const previousFocus = tree.getFocus(); - focusDown(accessor, arg2); - - // Then adjust selection - expandMultiSelection(focused, previousFocus); - } - } - }); - - function focusUp(accessor: ServicesAccessor, arg2?: number): void { - const focused = accessor.get(IListService).lastFocusedList; - const count = typeof arg2 === 'number' ? arg2 : 1; - - // Ensure DOM Focus - ensureDOMFocus(focused); - - // List - if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - - list.focusPrevious(count); - const listFocus = list.getFocus(); - if (listFocus.length) { - list.reveal(listFocus[0]); - } - } - - // ObjectTree - else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const tree = focused; - - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - tree.focusPrevious(count, true, fakeKeyboardEvent); - - const listFocus = tree.getFocus(); - if (listFocus.length) { - tree.reveal(listFocus[0]); - } - } - - // Tree - else if (focused) { - const tree = focused; - - tree.focusPrevious(count, { origin: 'keyboard' }); - tree.reveal(tree.getFocus()); - } - } - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.focusUp', - weight: KeybindingWeight.WorkbenchContrib, - when: WorkbenchListFocusContextKey, - primary: KeyCode.UpArrow, - mac: { - primary: KeyCode.UpArrow, - secondary: [KeyMod.WinCtrl | KeyCode.KEY_P] - }, - handler: (accessor, arg2) => focusUp(accessor, arg2) - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.expandSelectionUp', - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListSupportsMultiSelectContextKey), - primary: KeyMod.Shift | KeyCode.UpArrow, - handler: (accessor, arg2) => { - const focused = accessor.get(IListService).lastFocusedList; - - // List - if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - - // Focus up first - const previousFocus = list.getFocus() ? list.getFocus()[0] : undefined; - focusUp(accessor, arg2); - - // Then adjust selection - expandMultiSelection(focused, previousFocus); - } - - // Tree - else if (focused) { - const tree = focused; - - // Focus up first - const previousFocus = tree.getFocus(); - focusUp(accessor, arg2); - - // Then adjust selection - expandMultiSelection(focused, previousFocus); - } - } - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.collapse', - weight: KeybindingWeight.WorkbenchContrib, - when: WorkbenchListFocusContextKey, - primary: KeyCode.LeftArrow, - mac: { - primary: KeyCode.LeftArrow, - secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow] - }, - handler: (accessor) => { - const focused = accessor.get(IListService).lastFocusedList; - - // Tree only - if (focused && !(focused instanceof List || focused instanceof PagedList)) { - if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const tree = focused; - const focusedElements = tree.getFocus(); - - if (focusedElements.length === 0) { - return; - } - - const focus = focusedElements[0]; - - if (!tree.collapse(focus)) { - const parent = tree.getParentElement(focus); - - if (parent) { - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - tree.setFocus([parent], fakeKeyboardEvent); - tree.reveal(parent); - } - } - } else { - const tree = focused; - const focus = tree.getFocus(); - - tree.collapse(focus).then(didCollapse => { - if (focus && !didCollapse) { - tree.focusParent({ origin: 'keyboard' }); - - return tree.reveal(tree.getFocus()); - } - - return undefined; - }); - } - } - } - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.expand', - weight: KeybindingWeight.WorkbenchContrib, - when: WorkbenchListFocusContextKey, - primary: KeyCode.RightArrow, - handler: (accessor) => { - const focused = accessor.get(IListService).lastFocusedList; - - // Tree only - if (focused && !(focused instanceof List || focused instanceof PagedList)) { - if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const tree = focused; - const focusedElements = tree.getFocus(); - - if (focusedElements.length === 0) { - return; - } - - const focus = focusedElements[0]; - - if (!tree.expand(focus)) { - const child = tree.getFirstElementChild(focus); - - if (child) { - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - tree.setFocus([child], fakeKeyboardEvent); - tree.reveal(child); - } - } - } else { - const tree = focused; - const focus = tree.getFocus(); - - tree.expand(focus).then(didExpand => { - if (focus && !didExpand) { - tree.focusFirstChild({ origin: 'keyboard' }); - - return tree.reveal(tree.getFocus()); - } - - return undefined; - }); - } - } - } - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.focusPageUp', - weight: KeybindingWeight.WorkbenchContrib, - when: WorkbenchListFocusContextKey, - primary: KeyCode.PageUp, - handler: (accessor) => { - const focused = accessor.get(IListService).lastFocusedList; - - // Ensure DOM Focus - ensureDOMFocus(focused); - - // List - if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - - list.focusPreviousPage(); - list.reveal(list.getFocus()[0]); - } - - // ObjectTree - else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const list = focused; - - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - list.focusPreviousPage(fakeKeyboardEvent); - list.reveal(list.getFocus()[0]); - } - - // Tree - else if (focused) { - const tree = focused; - - tree.focusPreviousPage({ origin: 'keyboard' }); - tree.reveal(tree.getFocus()); - } - } - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.focusPageDown', - weight: KeybindingWeight.WorkbenchContrib, - when: WorkbenchListFocusContextKey, - primary: KeyCode.PageDown, - handler: (accessor) => { - const focused = accessor.get(IListService).lastFocusedList; - - // Ensure DOM Focus - ensureDOMFocus(focused); - - // List - if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - - list.focusNextPage(); - list.reveal(list.getFocus()[0]); - } - - // ObjectTree - else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const list = focused; - - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - list.focusNextPage(fakeKeyboardEvent); - list.reveal(list.getFocus()[0]); - } - - // Tree - else if (focused) { - const tree = focused; - - tree.focusNextPage({ origin: 'keyboard' }); - tree.reveal(tree.getFocus()); - } - } - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.focusFirst', - weight: KeybindingWeight.WorkbenchContrib, - when: WorkbenchListFocusContextKey, - primary: KeyCode.Home, - handler: accessor => listFocusFirst(accessor) - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.focusFirstChild', - weight: KeybindingWeight.WorkbenchContrib, - when: WorkbenchListFocusContextKey, - primary: 0, - handler: accessor => listFocusFirst(accessor, { fromFocused: true }) - }); - - function listFocusFirst(accessor: ServicesAccessor, options?: { fromFocused: boolean }): void { - const focused = accessor.get(IListService).lastFocusedList; - - // Ensure DOM Focus - ensureDOMFocus(focused); - - // List - if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - - list.setFocus([0]); - list.reveal(0); - } - - // ObjectTree - else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const tree = focused; - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - tree.focusFirst(fakeKeyboardEvent); - - const focus = tree.getFocus(); - - if (focus.length > 0) { - tree.reveal(focus[0]); - } - } - - // Tree - else if (focused) { - const tree = focused; - - tree.focusFirst({ origin: 'keyboard' }, options && options.fromFocused ? tree.getFocus() : undefined); - tree.reveal(tree.getFocus()); - } - } - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.focusLast', - weight: KeybindingWeight.WorkbenchContrib, - when: WorkbenchListFocusContextKey, - primary: KeyCode.End, - handler: accessor => listFocusLast(accessor) - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.focusLastChild', - weight: KeybindingWeight.WorkbenchContrib, - when: WorkbenchListFocusContextKey, - primary: 0, - handler: accessor => listFocusLast(accessor, { fromFocused: true }) - }); - - function listFocusLast(accessor: ServicesAccessor, options?: { fromFocused: boolean }): void { - const focused = accessor.get(IListService).lastFocusedList; - - // Ensure DOM Focus - ensureDOMFocus(focused); - - // List - if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - - list.setFocus([list.length - 1]); - list.reveal(list.length - 1); - } - - // ObjectTree - else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const tree = focused; - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - tree.focusLast(fakeKeyboardEvent); - - const focus = tree.getFocus(); - - if (focus.length > 0) { - tree.reveal(focus[0]); - } - } - - // Tree - else if (focused) { - const tree = focused; - - tree.focusLast({ origin: 'keyboard' }, options && options.fromFocused ? tree.getFocus() : undefined); - tree.reveal(tree.getFocus()); - } - } - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.select', - weight: KeybindingWeight.WorkbenchContrib, - when: WorkbenchListFocusContextKey, - primary: KeyCode.Enter, - mac: { - primary: KeyCode.Enter, - secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow] - }, - handler: (accessor) => { - const focused = accessor.get(IListService).lastFocusedList; - - // List - if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - list.setSelection(list.getFocus()); - list.open(list.getFocus()); - } - - // ObjectTree - else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const list = focused; - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - list.setSelection(list.getFocus(), fakeKeyboardEvent); - list.open(list.getFocus()); - } - - // Tree - else if (focused) { - const tree = focused; - const focus = tree.getFocus(); - - if (focus) { - tree.setSelection([focus], { origin: 'keyboard' }); - } - } - } - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.selectAll', - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListSupportsMultiSelectContextKey), - primary: KeyMod.CtrlCmd | KeyCode.KEY_A, - handler: (accessor) => { - const focused = accessor.get(IListService).lastFocusedList; - - // List - if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - list.setSelection(range(list.length)); - } - - // Trees - else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const tree = focused; - const focus = tree.getFocus(); - const selection = tree.getSelection(); - - // Which element should be considered to start selecting all? - let start: any | undefined = undefined; - - if (focus.length > 0 && (selection.length === 0 || selection.indexOf(focus[0]) === -1)) { - start = focus[0]; - } - - if (!start && selection.length > 0) { - start = selection[0]; - } - - // What is the scope of select all? - let scope: any | undefined = undefined; - - if (!start) { - scope = undefined; - } else { - const selectedNode = tree.getNode(start); - const parentNode = selectedNode.parent; - - if (!parentNode.parent) { // root - scope = undefined; - } else { - scope = parentNode.element; - } - } - - const newSelection: any[] = []; - - // If the scope isn't the tree root, it should be part of the new selection - if (scope) { - newSelection.push(scope); - } - - const visit = (node: ITreeNode) => { - for (const child of node.children) { - if (child.visible) { - newSelection.push(child.element); - - if (!child.collapsed) { - visit(child); - } - } - } - }; - - // Add the whole scope subtree to the new selection - visit(tree.getNode(scope)); - - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - tree.setSelection(newSelection, fakeKeyboardEvent); - } - } - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.toggleExpand', - weight: KeybindingWeight.WorkbenchContrib, - when: WorkbenchListFocusContextKey, - primary: KeyCode.Space, - handler: (accessor) => { - const focused = accessor.get(IListService).lastFocusedList; - - // Tree only - if (focused && !(focused instanceof List || focused instanceof PagedList)) { - if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const tree = focused; - const focus = tree.getFocus(); - - if (focus.length === 0) { - return; - } - - tree.toggleCollapsed(focus[0]); - } else { - const tree = focused; - const focus = tree.getFocus(); - - if (focus) { - tree.toggleExpansion(focus); - } - } - } - } - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'list.clear', - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListHasSelectionOrFocus), - primary: KeyCode.Escape, - handler: (accessor) => { - const focused = accessor.get(IListService).lastFocusedList; - - // List - if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - - if (list.getSelection().length > 0) { - list.setSelection([]); - } else if (list.getFocus().length > 0) { - list.setFocus([]); - } - } - - // ObjectTree - else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const list = focused; - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - - if (list.getSelection().length > 0) { - list.setSelection([], fakeKeyboardEvent); - } else if (list.getFocus().length > 0) { - list.setFocus([], fakeKeyboardEvent); - } - } - - // Tree - else if (focused) { - const tree = focused; - - if (tree.getSelection().length) { - tree.clearSelection({ origin: 'keyboard' }); - } else if (tree.getFocus()) { - tree.clearFocus({ origin: 'keyboard' }); - } - } - } - }); - - // --- commands - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'workbench.action.closeWindow', // close the window when the last editor is closed by reusing the same keybinding - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(NoEditorsVisibleContext, SingleEditorGroupsContext), - primary: KeyMod.CtrlCmd | KeyCode.KEY_W, - handler: accessor => { - const windowService = accessor.get(IWindowService); - windowService.closeWindow(); - } - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'workbench.action.exitZenMode', - weight: KeybindingWeight.EditorContrib - 1000, - handler(accessor: ServicesAccessor, configurationOrName: any) { - const partService = accessor.get(IPartService); - partService.toggleZenMode(); - }, - when: InEditorZenModeContext, - primary: KeyChord(KeyCode.Escape, KeyCode.Escape) - }); - - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: QUIT_ID, - weight: KeybindingWeight.WorkbenchContrib, - handler(accessor: ServicesAccessor) { - const windowsService = accessor.get(IWindowsService); - windowsService.quit(); - }, - when: undefined, - primary: KeyMod.CtrlCmd | KeyCode.KEY_Q, - win: { primary: undefined } - }); - - CommandsRegistry.registerCommand('_workbench.removeFromRecentlyOpened', function (accessor: ServicesAccessor, path: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string) { - const windowsService = accessor.get(IWindowsService); - - return windowsService.removeFromRecentlyOpened([path]).then(() => undefined); - }); - - CommandsRegistry.registerCommand('_workbench.downloadResource', function (accessor: ServicesAccessor, resource: URI) { - const downloadService = accessor.get(IDownloadService); - const location = posix.join(tmpdir(), generateUuid()); - - return downloadService.download(resource, location).then(() => URI.file(location)); - }); -} diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index 0a7fc9f8db8..1e6333c7314 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import * as perf from 'vs/base/common/performance'; -import { WorkbenchShell } from 'vs/workbench/electron-browser/shell'; +import { Shell } from 'vs/workbench/electron-browser/shell'; import * as browser from 'vs/base/browser/browser'; import { domContentLoaded } from 'vs/base/browser/dom'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -127,7 +127,7 @@ function openWorkbench(configuration: IWindowConfiguration): Promise { perf.mark('willStartWorkbench'); // Create Shell - const shell = new WorkbenchShell(document.body, { + const shell = new Shell(document.body, { contextService: workspaceService, configurationService: workspaceService, environmentService, diff --git a/src/vs/workbench/electron-browser/media/clear.svg b/src/vs/workbench/electron-browser/media/clear.svg deleted file mode 100644 index c2f3e027100..00000000000 --- a/src/vs/workbench/electron-browser/media/clear.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/media/workbench.css b/src/vs/workbench/electron-browser/media/workbench.css index 28c4a57c000..951d0972276 100644 --- a/src/vs/workbench/electron-browser/media/workbench.css +++ b/src/vs/workbench/electron-browser/media/workbench.css @@ -11,7 +11,7 @@ overflow: hidden; } -.monaco-workbench > .part { +.monaco-workbench .part { position: absolute; box-sizing: border-box; } diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/shell.contribution.ts similarity index 61% rename from src/vs/workbench/electron-browser/main.contribution.ts rename to src/vs/workbench/electron-browser/shell.contribution.ts index f478a2cc03a..bc02c150a70 100644 --- a/src/vs/workbench/electron-browser/main.contribution.ts +++ b/src/vs/workbench/electron-browser/shell.contribution.ts @@ -12,19 +12,44 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions, Configur import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; -import { KeybindingsReferenceAction, OpenDocumentationUrlAction, OpenIntroductoryVideosUrlAction, OpenTipsAndTricksUrlAction, OpenIssueReporterAction, ReportPerformanceIssueUsingReporterAction, ZoomResetAction, ZoomOutAction, ZoomInAction, ToggleFullScreenAction, ToggleMenuBarAction, CloseWorkspaceAction, CloseCurrentWindowAction, SwitchWindow, NewWindowAction, NavigateUpAction, NavigateDownAction, NavigateLeftAction, NavigateRightAction, IncreaseViewSizeAction, DecreaseViewSizeAction, ToggleSharedProcessAction, QuickSwitchWindow, QuickOpenRecentAction, inRecentFilesPickerContextKey, ShowAboutDialogAction, InspectContextKeysAction, OpenProcessExplorer, OpenTwitterUrlAction, OpenRequestFeatureUrlAction, OpenPrivacyStatementUrlAction, OpenLicenseUrlAction, OpenRecentAction, ToggleScreencastModeAction } from 'vs/workbench/electron-browser/actions'; -import { registerCommands, QUIT_ID } from 'vs/workbench/electron-browser/commands'; -import { AddRootFolderAction, GlobalRemoveRootFolderAction, OpenWorkspaceAction, SaveWorkspaceAsAction, OpenWorkspaceConfigFileAction, DuplicateWorkspaceInNewWindowAction, OpenFileFolderAction, OpenFileAction, OpenFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; +import { KeybindingsReferenceAction, OpenDocumentationUrlAction, OpenIntroductoryVideosUrlAction, OpenTipsAndTricksUrlAction, OpenIssueReporterAction, ReportPerformanceIssueUsingReporterAction, OpenProcessExplorer, OpenTwitterUrlAction, OpenRequestFeatureUrlAction, OpenPrivacyStatementUrlAction, OpenLicenseUrlAction } from 'vs/workbench/electron-browser/actions/helpActions'; +import { ToggleSharedProcessAction, InspectContextKeysAction, ToggleScreencastModeAction } from 'vs/workbench/electron-browser/actions/developerActions'; +import { ShowAboutDialogAction, ZoomResetAction, ZoomOutAction, ZoomInAction, ToggleFullScreenAction, CloseCurrentWindowAction, SwitchWindow, NewWindowAction, QuickSwitchWindow, QuickOpenRecentAction, inRecentFilesPickerContextKey, OpenRecentAction } from 'vs/workbench/electron-browser/actions/windowActions'; +import { AddRootFolderAction, GlobalRemoveRootFolderAction, OpenWorkspaceAction, SaveWorkspaceAsAction, OpenWorkspaceConfigFileAction, DuplicateWorkspaceInNewWindowAction, OpenFileFolderAction, OpenFileAction, OpenFolderAction, CloseWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions'; import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { inQuickOpenContext, getQuickNavigateHandler } from 'vs/workbench/browser/parts/quickopen/quickopen'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ADD_ROOT_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { IsMacContext } from 'vs/platform/workbench/common/contextkeys'; +import { NoEditorsVisibleContext, SingleEditorGroupsContext } from 'vs/workbench/common/editor'; +import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; // Contribute Commands -registerCommands(); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.action.closeWindow', // close the window when the last editor is closed by reusing the same keybinding + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(NoEditorsVisibleContext, SingleEditorGroupsContext), + primary: KeyMod.CtrlCmd | KeyCode.KEY_W, + handler: accessor => { + const windowService = accessor.get(IWindowService); + windowService.closeWindow(); + } +}); + +const QUIT_ID = 'workbench.action.quit'; +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: QUIT_ID, + weight: KeybindingWeight.WorkbenchContrib, + handler(accessor: ServicesAccessor) { + const windowsService = accessor.get(IWindowsService); + windowsService.quit(); + }, + when: undefined, + primary: KeyMod.CtrlCmd | KeyCode.KEY_Q, + win: { primary: undefined } +}); // Contribute Global Actions const viewCategory = nls.localize('view', "View"); @@ -93,16 +118,6 @@ workbenchActionsRegistry.registerWorkbenchAction( ); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleFullScreenAction, ToggleFullScreenAction.ID, ToggleFullScreenAction.LABEL, { primary: KeyCode.F11, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_F } }), 'View: Toggle Full Screen', viewCategory); -if (isWindows || isLinux) { - workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleMenuBarAction, ToggleMenuBarAction.ID, ToggleMenuBarAction.LABEL), 'View: Toggle Menu Bar', viewCategory); -} -workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(NavigateUpAction, NavigateUpAction.ID, NavigateUpAction.LABEL, undefined), 'View: Navigate to the View Above', viewCategory); -workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(NavigateDownAction, NavigateDownAction.ID, NavigateDownAction.LABEL, undefined), 'View: Navigate to the View Below', viewCategory); -workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(NavigateLeftAction, NavigateLeftAction.ID, NavigateLeftAction.LABEL, undefined), 'View: Navigate to the View on the Left', viewCategory); -workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(NavigateRightAction, NavigateRightAction.ID, NavigateRightAction.LABEL, undefined), 'View: Navigate to the View on the Right', viewCategory); - -workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(IncreaseViewSizeAction, IncreaseViewSizeAction.ID, IncreaseViewSizeAction.LABEL, undefined), 'View: Increase Current View Size', viewCategory); -workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(DecreaseViewSizeAction, DecreaseViewSizeAction.ID, DecreaseViewSizeAction.LABEL, undefined), 'View: Decrease Current View Size', viewCategory); const workspacesCategory = nls.localize('workspaces', "Workspaces"); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(AddRootFolderAction, AddRootFolderAction.ID, AddRootFolderAction.LABEL), 'Workspaces: Add Folder to Workspace...', workspacesCategory); @@ -302,16 +317,6 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 1 }); -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '1_toggle_view', - command: { - id: ToggleMenuBarAction.ID, - title: nls.localize({ key: 'miToggleMenuBar', comment: ['&& denotes a mnemonic'] }, "Toggle Menu &&Bar") - }, - when: IsMacContext.toNegated(), - order: 4 -}); - // Zoom MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { @@ -466,235 +471,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { when: IsMacContext.toNegated() }); -// Configuration: Workbench -const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); - -configurationRegistry.registerConfiguration({ - 'id': 'workbench', - 'order': 7, - 'title': nls.localize('workbenchConfigurationTitle', "Workbench"), - 'type': 'object', - 'properties': { - 'workbench.editor.showTabs': { - 'type': 'boolean', - 'description': nls.localize('showEditorTabs', "Controls whether opened editors should show in tabs or not."), - 'default': true - }, - 'workbench.editor.highlightModifiedTabs': { - 'type': 'boolean', - 'description': nls.localize('highlightModifiedTabs', "Controls whether a top border is drawn on modified (dirty) editor tabs or not."), - 'default': false - }, - 'workbench.editor.labelFormat': { - 'type': 'string', - 'enum': ['default', 'short', 'medium', 'long'], - 'enumDescriptions': [ - nls.localize('workbench.editor.labelFormat.default', "Show the name of the file. When tabs are enabled and two files have the same name in one group the distinguishing sections of each file's path are added. When tabs are disabled, the path relative to the workspace folder is shown if the editor is active."), - nls.localize('workbench.editor.labelFormat.short', "Show the name of the file followed by its directory name."), - nls.localize('workbench.editor.labelFormat.medium', "Show the name of the file followed by its path relative to the workspace folder."), - nls.localize('workbench.editor.labelFormat.long', "Show the name of the file followed by its absolute path.") - ], - 'default': 'default', - 'description': nls.localize({ - comment: ['This is the description for a setting. Values surrounded by parenthesis are not to be translated.'], - key: 'tabDescription' - }, "Controls the format of the label for an editor."), - }, - 'workbench.editor.tabCloseButton': { - 'type': 'string', - 'enum': ['left', 'right', 'off'], - 'default': 'right', - 'description': 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'.") - }, - 'workbench.editor.tabSizing': { - 'type': 'string', - 'enum': ['fit', 'shrink'], - 'default': 'fit', - 'enumDescriptions': [ - 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.") - ], - 'description': 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.") - }, - 'workbench.editor.closeTabsInMRUOrder': { - 'type': 'boolean', - 'description': nls.localize('closeTabsInMRUOrder', "Controls whether tabs are closed in most recently used order or from left to right."), - 'default': true - }, - 'workbench.editor.showIcons': { - 'type': 'boolean', - 'description': nls.localize('showIcons', "Controls whether opened editors should show with an icon or not. This requires an icon theme to be enabled as well."), - 'default': true - }, - 'workbench.editor.enablePreview': { - 'type': 'boolean', - 'description': nls.localize('enablePreview', "Controls whether opened editors show as preview. Preview editors are reused until they are pinned (e.g. via double click or editing) and show up with an italic font style."), - 'default': true - }, - 'workbench.editor.enablePreviewFromQuickOpen': { - 'type': 'boolean', - 'description': nls.localize('enablePreviewFromQuickOpen', "Controls whether opened editors from Quick Open show as preview. Preview editors are reused until they are pinned (e.g. via double click or editing)."), - 'default': true - }, - 'workbench.editor.closeOnFileDelete': { - 'type': 'boolean', - 'description': nls.localize('closeOnFileDelete', "Controls whether editors showing a file that was opened during the session should close automatically when getting deleted or renamed by some other process. Disabling this will keep the editor open on such an event. Note that deleting from within the application will always close the editor and that dirty files will never close to preserve your data."), - 'default': false - }, - 'workbench.editor.openPositioning': { - 'type': 'string', - 'enum': ['left', 'right', 'first', 'last'], - 'default': 'right', - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'editorOpenPositioning' }, "Controls where editors open. Select `left` or `right` to open editors to the left or right of the currently active one. Select `first` or `last` to open editors independently from the currently active one.") - }, - 'workbench.editor.openSideBySideDirection': { - 'type': 'string', - 'enum': ['right', 'down'], - 'default': 'right', - 'markdownDescription': nls.localize('sideBySideDirection', "Controls the default direction of editors that are opened side by side (e.g. from the explorer). By default, editors will open on the right hand side of the currently active one. If changed to `down`, the editors will open below the currently active one.") - }, - 'workbench.editor.closeEmptyGroups': { - 'type': 'boolean', - 'description': nls.localize('closeEmptyGroups', "Controls the behavior of empty editor groups when the last tab in the group is closed. When enabled, empty groups will automatically close. When disabled, empty groups will remain part of the grid."), - 'default': true - }, - 'workbench.editor.revealIfOpen': { - 'type': 'boolean', - 'description': nls.localize('revealIfOpen', "Controls whether an editor is revealed in any of the visible groups if opened. If disabled, an editor will prefer to open in the currently active editor group. If enabled, an already opened editor will be revealed instead of opened again in the currently active editor group. Note that there are some cases where this setting is ignored, e.g. when forcing an editor to open in a specific group or to the side of the currently active group."), - 'default': false - }, - 'workbench.editor.swipeToNavigate': { - 'type': 'boolean', - 'description': nls.localize('swipeToNavigate', "Navigate between open files using three-finger swipe horizontally."), - 'default': false, - 'included': isMacintosh - }, - 'workbench.editor.restoreViewState': { - 'type': 'boolean', - 'description': nls.localize('restoreViewState', "Restores the last view state (e.g. scroll position) when re-opening files after they have been closed."), - 'default': true, - }, - 'workbench.editor.centeredLayoutAutoResize': { - 'type': 'boolean', - 'default': true, - 'description': nls.localize('centeredLayoutAutoResize', "Controls if the centered layout should automatically resize to maximum width when more than one group is open. Once only one group is open it will resize back to the original centered width.") - }, - 'workbench.commandPalette.history': { - 'type': 'number', - 'description': nls.localize('commandHistory', "Controls the number of recently used commands to keep in history for the command palette. Set to 0 to disable command history."), - 'default': 50 - }, - 'workbench.commandPalette.preserveInput': { - 'type': 'boolean', - 'description': nls.localize('preserveInput', "Controls whether the last typed input to the command palette should be restored when opening it the next time."), - 'default': false - }, - 'workbench.quickOpen.closeOnFocusLost': { - 'type': 'boolean', - 'description': nls.localize('closeOnFocusLost', "Controls whether Quick Open should close automatically once it loses focus."), - 'default': true - }, - 'workbench.quickOpen.preserveInput': { - 'type': 'boolean', - 'description': nls.localize('workbench.quickOpen.preserveInput', "Controls whether the last typed input to Quick Open should be restored when opening it the next time."), - 'default': false - }, - 'workbench.settings.openDefaultSettings': { - 'type': 'boolean', - 'description': nls.localize('openDefaultSettings', "Controls whether opening settings also opens an editor showing all default settings."), - 'default': false - }, - 'workbench.settings.useSplitJSON': { - 'type': 'boolean', - 'markdownDescription': nls.localize('useSplitJSON', "Controls whether to use the split JSON editor when editing settings as JSON."), - 'default': false - }, - 'workbench.settings.openDefaultKeybindings': { - 'type': 'boolean', - 'description': nls.localize('openDefaultKeybindings', "Controls whether opening keybinding settings also opens an editor showing all default keybindings."), - 'default': true - }, - 'workbench.sideBar.location': { - 'type': 'string', - 'enum': ['left', 'right'], - 'default': 'left', - 'description': nls.localize('sideBarLocation', "Controls the location of the sidebar. It can either show on the left or right of the workbench.") - }, - 'workbench.panel.defaultLocation': { - 'type': 'string', - 'enum': ['bottom', 'right'], - 'default': 'bottom', - 'description': nls.localize('panelDefaultLocation', "Controls the default location of the panel (terminal, debug console, output, problems). It can either show at the bottom or on the right of the workbench.") - }, - 'workbench.statusBar.visible': { - 'type': 'boolean', - 'default': true, - 'description': nls.localize('statusBarVisibility', "Controls the visibility of the status bar at the bottom of the workbench.") - }, - 'workbench.activityBar.visible': { - 'type': 'boolean', - 'default': true, - 'description': nls.localize('activityBarVisibility', "Controls the visibility of the activity bar in the workbench.") - }, - 'workbench.view.alwaysShowHeaderActions': { - 'type': 'boolean', - 'default': false, - 'description': nls.localize('viewVisibility', "Controls the visibility of view header actions. View header actions may either be always visible, or only visible when that view is focused or hovered over.") - }, - 'workbench.fontAliasing': { - 'type': 'string', - 'enum': ['default', 'antialiased', 'none', 'auto'], - 'default': 'default', - 'description': - nls.localize('fontAliasing', "Controls font aliasing method in the workbench."), - 'enumDescriptions': [ - nls.localize('workbench.fontAliasing.default', "Sub-pixel font smoothing. On most non-retina displays this will give the sharpest text."), - nls.localize('workbench.fontAliasing.antialiased', "Smooth the font on the level of the pixel, as opposed to the subpixel. Can make the font appear lighter overall."), - nls.localize('workbench.fontAliasing.none', "Disables font smoothing. Text will show with jagged sharp edges."), - nls.localize('workbench.fontAliasing.auto', "Applies `default` or `antialiased` automatically based on the DPI of displays.") - ], - 'included': isMacintosh - }, - 'workbench.settings.enableNaturalLanguageSearch': { - 'type': 'boolean', - 'description': nls.localize('enableNaturalLanguageSettingsSearch', "Controls whether to enable the natural language search mode for settings. The natural language search is provided by a Microsoft online service."), - 'default': true, - 'scope': ConfigurationScope.WINDOW, - 'tags': ['usesOnlineServices'] - }, - 'workbench.settings.settingsSearchTocBehavior': { - 'type': 'string', - 'enum': ['hide', 'filter'], - 'enumDescriptions': [ - nls.localize('settingsSearchTocBehavior.hide', "Hide the Table of Contents while searching."), - nls.localize('settingsSearchTocBehavior.filter', "Filter the Table of Contents to just categories that have matching settings. Clicking a category will filter the results to that category."), - ], - 'description': nls.localize('settingsSearchTocBehavior', "Controls the behavior of the settings editor Table of Contents while searching."), - 'default': 'filter', - 'scope': ConfigurationScope.WINDOW - }, - 'workbench.settings.editor': { - 'type': 'string', - 'enum': ['ui', 'json'], - 'enumDescriptions': [ - nls.localize('settings.editor.ui', "Use the settings UI editor."), - nls.localize('settings.editor.json', "Use the JSON file editor."), - ], - 'description': nls.localize('settings.editor.desc', "Determines which settings editor to use by default."), - 'default': 'ui', - 'scope': ConfigurationScope.WINDOW - }, - 'workbench.enableExperiments': { - 'type': 'boolean', - 'description': nls.localize('workbench.enableExperiments', "Fetches experiments to run from a Microsoft online service."), - 'default': true, - 'tags': ['usesOnlineServices'] - } - } -}); - // Configuration: Window - +const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ 'id': 'window', 'order': 8, @@ -769,7 +547,7 @@ configurationRegistry.registerConfiguration({ 'type': 'string', 'default': isMacintosh ? '${activeEditorShort}${separator}${rootName}' : '${dirty}${activeEditorShort}${separator}${rootName}${separator}${appName}', 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by parenthesis are not to be translated.'], key: 'title' }, - "Controls the window title based on the active editor. Variables are substituted based on the context:\n- `\${activeEditorShort}`: the file name (e.g. myFile.txt).\n- `\${activeEditorMedium}`: the path of the file relative to the workspace folder (e.g. myFolder/myFile.txt).\n- `\${activeEditorLong}`: the full path of the file (e.g. /Users/Development/myProject/myFolder/myFile.txt).\n- `\${folderName}`: name of the workspace folder the file is contained in (e.g. myFolder).\n- `\${folderPath}`: file path of the workspace folder the file is contained in (e.g. /Users/Development/myFolder).\n- `\${rootName}`: name of the workspace (e.g. myFolder or myWorkspace).\n- `\${rootPath}`: file path of the workspace (e.g. /Users/Development/myWorkspace).\n- `\${appName}`: e.g. VS Code.\n- `\${dirty}`: a dirty indicator if the active editor is dirty.\n- `\${separator}`: a conditional separator (\" - \") that only shows when surrounded by variables with values or static text.") + "Controls the window title based on the active editor. Variables are substituted based on the context:\n- `\${activeEditorShort}`: the file name (e.g. myFile.txt).\n- `\${activeEditorMedium}`: the path of the file relative to the workspace folder (e.g. myFolder/myFileFolder/myFile.txt).\n- `\${activeEditorLong}`: the full path of the file (e.g. /Users/Development/myFolder/myFileFolder/myFile.txt).\n- `\${activeFolderShort}`: the name of the folder the file is contained in (e.g. myFileFolder).\n- `\${activeFolderMedium}`: the path of the folder the file is contained in, relative to the workspace folder (e.g. myFolder/myFileFolder).\n- `\${activeFolderLong}`: the full path of the folder the file is contained in (e.g. /Users/Development/myFolder/myFileFolder).\n- `\${folderName}`: name of the workspace folder the file is contained in (e.g. myFolder).\n- `\${folderPath}`: file path of the workspace folder the file is contained in (e.g. /Users/Development/myFolder).\n- `\${rootName}`: name of the workspace (e.g. myFolder or myWorkspace).\n- `\${rootPath}`: file path of the workspace (e.g. /Users/Development/myWorkspace).\n- `\${appName}`: e.g. VS Code.\n- `\${dirty}`: a dirty indicator if the active editor is dirty.\n- `\${separator}`: a conditional separator (\" - \") that only shows when surrounded by variables with values or static text.") }, 'window.newWindowDimensions': { 'type': 'string', @@ -820,7 +598,7 @@ configurationRegistry.registerConfiguration({ 'type': 'boolean', 'default': false, 'scope': ConfigurationScope.APPLICATION, - 'description': nls.localize('window.doubleClickIconToClose', "If enabled, double clicking the application icon in the title bar will close the window and the window cannot be dragged by the icon. This setting only has an effect when `#window.titleBarStyle#` is set to `custom`.") + 'markdownDescription': nls.localize('window.doubleClickIconToClose', "If enabled, double clicking the application icon in the title bar will close the window and the window cannot be dragged by the icon. This setting only has an effect when `#window.titleBarStyle#` is set to `custom`.") }, 'window.titleBarStyle': { 'type': 'string', @@ -842,13 +620,6 @@ configurationRegistry.registerConfiguration({ 'description': nls.localize('window.nativeFullScreen', "Controls if native full-screen should be used on macOS. Disable this option to prevent macOS from creating a new space when going full-screen."), 'included': isMacintosh }, - 'window.smoothScrollingWorkaround': { // TODO@Ben remove once https://github.com/Microsoft/vscode/issues/61824 settles - 'type': 'boolean', - 'default': false, - 'scope': ConfigurationScope.APPLICATION, - 'markdownDescription': nls.localize('window.smoothScrollingWorkaround', "Enable this workaround if scrolling is no longer smooth after restoring a minimized VS Code window. This is a workaround for an issue (https://github.com/Microsoft/vscode/issues/13612) where scrolling starts to lag on devices with precision trackpads like the Surface devices from Microsoft. Enabling this workaround can result in a little bit of layout flickering after restoring the window from minimized state but is otherwise harmless."), - 'included': isWindows - }, 'window.clickThroughInactive': { 'type': 'boolean', 'default': true, @@ -859,47 +630,3 @@ configurationRegistry.registerConfiguration({ } }); -// Configuration: Zen Mode -configurationRegistry.registerConfiguration({ - 'id': 'zenMode', - 'order': 9, - 'title': nls.localize('zenModeConfigurationTitle', "Zen Mode"), - 'type': 'object', - 'properties': { - 'zenMode.fullScreen': { - 'type': 'boolean', - 'default': true, - 'description': nls.localize('zenMode.fullScreen', "Controls whether turning on Zen Mode also puts the workbench into full screen mode.") - }, - 'zenMode.centerLayout': { - 'type': 'boolean', - 'default': true, - 'description': nls.localize('zenMode.centerLayout', "Controls whether turning on Zen Mode also centers the layout.") - }, - 'zenMode.hideTabs': { - 'type': 'boolean', - 'default': true, - 'description': nls.localize('zenMode.hideTabs', "Controls whether turning on Zen Mode also hides workbench tabs.") - }, - 'zenMode.hideStatusBar': { - 'type': 'boolean', - 'default': true, - 'description': nls.localize('zenMode.hideStatusBar', "Controls whether turning on Zen Mode also hides the status bar at the bottom of the workbench.") - }, - 'zenMode.hideActivityBar': { - 'type': 'boolean', - 'default': true, - 'description': nls.localize('zenMode.hideActivityBar', "Controls whether turning on Zen Mode also hides the activity bar at the left of the workbench.") - }, - 'zenMode.hideLineNumbers': { - 'type': 'boolean', - 'default': true, - 'description': nls.localize('zenMode.hideLineNumbers', "Controls whether turning on Zen Mode also hides the editor line numbers.") - }, - 'zenMode.restore': { - 'type': 'boolean', - 'default': false, - 'description': nls.localize('zenMode.restore', "Controls whether a window should restore to zen mode if it was exited in zen mode.") - } - } -}); diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index 3b28eced7e7..fd959395067 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -123,7 +123,7 @@ export interface ICoreServices { * The workbench shell contains the workbench with a rich header containing navigation and the activity bar. * With the Shell being the top level element in the page, it is also responsible for driving the layouting. */ -export class WorkbenchShell extends Disposable { +export class Shell extends Disposable { private readonly _onWillShutdown = this._register(new Emitter()); get onWillShutdown(): Event { return this._onWillShutdown.event; } @@ -416,11 +416,12 @@ export class WorkbenchShell extends Disposable { serviceCollection.set(IWindowService, new SyncDescriptor(WindowService, [this.configuration.windowId, this.configuration])); const sharedProcess = (serviceCollection.get(IWindowsService)).whenSharedProcessReady() - .then(() => connectNet(this.environmentService.sharedIPCHandle, `window:${this.configuration.windowId}`)); + .then(() => connectNet(this.environmentService.sharedIPCHandle, `window:${this.configuration.windowId}`)) + .then(client => { + client.registerChannel('dialog', instantiationService.createInstance(DialogChannel)); - sharedProcess.then(client => { - client.registerChannel('dialog', instantiationService.createInstance(DialogChannel)); - }); + return client; + }); // Hash serviceCollection.set(IHashService, new SyncDescriptor(HashService, undefined, true)); diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 8fd07d1e1e4..f20aff0ed3b 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -27,14 +27,13 @@ import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart'; import { StatusbarPart } from 'vs/workbench/browser/parts/statusbar/statusbarPart'; import { TitlebarPart } from 'vs/workbench/browser/parts/titlebar/titlebarPart'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; -import { WorkbenchLayout } from 'vs/workbench/browser/layout'; import { IActionBarRegistry, Extensions as ActionBarExtensions } from 'vs/workbench/browser/actions'; import { PanelRegistry, Extensions as PanelExtensions } from 'vs/workbench/browser/panel'; import { QuickOpenController } from 'vs/workbench/browser/parts/quickopen/quickOpenController'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { QuickInputService } from 'vs/workbench/browser/parts/quickinput/quickInput'; import { getServices } from 'vs/platform/instantiation/common/extensions'; -import { Position, Parts, IPartService, ILayoutOptions, IDimension, PositionToString } from 'vs/workbench/services/part/common/partService'; +import { Position, Parts, IPartService, IDimension, PositionToString, ILayoutOptions } from 'vs/workbench/services/part/common/partService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason } from 'vs/platform/storage/common/storage'; import { ContextMenuService as NativeContextMenuService } from 'vs/workbench/services/contextview/electron-browser/contextmenuService'; @@ -80,7 +79,8 @@ import { MenuService } from 'vs/platform/actions/common/menuService'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { OpenRecentAction, ToggleDevToolsAction, ReloadWindowAction, ShowPreviousWindowTab, MoveWindowTabToNewWindow, MergeAllWindowTabs, ShowNextWindowTab, ToggleWindowTabsBar, ReloadWindowWithExtensionsDisabledAction, NewWindowTab } from 'vs/workbench/electron-browser/actions'; +import { ShowPreviousWindowTab, MoveWindowTabToNewWindow, MergeAllWindowTabs, ShowNextWindowTab, ToggleWindowTabsBar, NewWindowTab, OpenRecentAction, ReloadWindowAction, ReloadWindowWithExtensionsDisabledAction } from 'vs/workbench/electron-browser/actions/windowActions'; +import { ToggleDevToolsAction } from 'vs/workbench/electron-browser/actions/developerActions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; import { WorkspaceEditingService } from 'vs/workbench/services/workspace/node/workspaceEditingService'; @@ -113,7 +113,9 @@ import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/work import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { FileDialogService } from 'vs/workbench/services/dialogs/electron-browser/dialogService'; import { LogStorageAction } from 'vs/platform/storage/node/storageService'; +import { Sizing, Direction, Grid, View } from 'vs/base/browser/ui/grid/grid'; import { IEditor } from 'vs/editor/common/editorCommon'; +import { WorkbenchLayout } from 'vs/workbench/browser/layout'; interface WorkbenchParams { configuration: IWindowConfiguration; @@ -168,6 +170,12 @@ interface IZenMode { wasPanelVisible: boolean; } +interface IWorkbenchUIState { + lastPanelHeight?: number; + lastPanelWidth?: number; + lastSidebarDimension?: number; +} + export class Workbench extends Disposable implements IPartService { private static readonly sidebarHiddenStorageKey = 'workbench.sidebar.hidden'; @@ -200,7 +208,7 @@ export class Workbench extends Disposable implements IPartService { private fileService: IFileService; private quickInput: QuickInputService; - private workbenchLayout: WorkbenchLayout; + private workbenchGrid: Grid | WorkbenchLayout; private titlebarPart: TitlebarPart; private activitybarPart: ActivitybarPart; @@ -208,10 +216,19 @@ export class Workbench extends Disposable implements IPartService { private panelPart: PanelPart; private editorPart: EditorPart; private statusbarPart: StatusbarPart; + + private titlebarPartView: View; + private activitybarPartView: View; + private sidebarPartView: View; + private panelPartView: View; + private editorPartView: View; + private statusbarPartView: View; + private quickOpen: QuickOpenController; private notificationsCenter: NotificationsCenter; private notificationsToasts: NotificationsToasts; + private editorHidden: boolean; private sideBarHidden: boolean; private statusBarHidden: boolean; private activityBarHidden: boolean; @@ -224,6 +241,11 @@ export class Workbench extends Disposable implements IPartService { private fontAliasing: FontAliasingOption; private hasInitialFilesToOpen: boolean; private shouldCenterLayout = false; + private uiState: IWorkbenchUIState = { + lastPanelHeight: 350, + lastPanelWidth: 350, + lastSidebarDimension: 300, + }; private inZenMode: IContextKey; private sideBarVisibleContext: IContextKey; @@ -890,6 +912,9 @@ export class Workbench extends Disposable implements IPartService { private initSettings(): void { + // Editor visiblity + this.editorHidden = false; + // Sidebar visibility this.sideBarHidden = this.storageService.getBoolean(Workbench.sidebarHiddenStorageKey, StorageScope.WORKSPACE, this.contextService.getWorkbenchState() === WorkbenchState.EMPTY); @@ -944,6 +969,22 @@ export class Workbench extends Disposable implements IPartService { return getTitleBarStyle(this.configurationService, this.environmentService) === 'custom'; } + private saveLastPanelDimension(): void { + if (!(this.workbenchGrid instanceof Grid)) { + return; + } + + if (this.panelPosition === Position.BOTTOM) { + this.uiState.lastPanelHeight = this.workbenchGrid.getViewSize(this.panelPartView); + } else { + this.uiState.lastPanelWidth = this.workbenchGrid.getViewSize(this.panelPartView); + } + } + + private getLastPanelDimension(position: Position): number | undefined { + return position === Position.BOTTOM ? this.uiState.lastPanelHeight : this.uiState.lastPanelWidth; + } + private setStatusBarHidden(hidden: boolean, skipLayout?: boolean): void { this.statusBarHidden = hidden; @@ -956,7 +997,11 @@ export class Workbench extends Disposable implements IPartService { // Layout if (!skipLayout) { - this.workbenchLayout.layout(); + if (this.workbenchGrid instanceof Grid) { + this.layout(); + } else { + this.workbenchGrid.layout(); + } } } @@ -973,23 +1018,38 @@ export class Workbench extends Disposable implements IPartService { } private createWorkbenchLayout(): void { - this.workbenchLayout = this.instantiationService.createInstance( - WorkbenchLayout, - this.container, - this.workbench, - { - titlebar: this.titlebarPart, - activitybar: this.activitybarPart, - editor: this.editorPart, - sidebar: this.sidebarPart, - panel: this.panelPart, - statusbar: this.statusbarPart, - }, - this.quickOpen, - this.quickInput, - this.notificationsCenter, - this.notificationsToasts - ); + if (this.configurationService.getValue('workbench.useExperimentalGridLayout')) { + // Create view wrappers for all parts + this.titlebarPartView = new View(this.titlebarPart); + this.sidebarPartView = new View(this.sidebarPart); + this.activitybarPartView = new View(this.activitybarPart); + this.editorPartView = new View(this.editorPart); + this.panelPartView = new View(this.panelPart); + this.statusbarPartView = new View(this.statusbarPart); + + this.workbenchGrid = new Grid(this.editorPartView, { proportionalLayout: false }); + + this.workbench.prepend(this.workbenchGrid.element); + this.layout(); + } else { + this.workbenchGrid = this.instantiationService.createInstance( + WorkbenchLayout, + this.container, + this.workbench, + { + titlebar: this.titlebarPart, + activitybar: this.activitybarPart, + editor: this.editorPart, + sidebar: this.sidebarPart, + panel: this.panelPart, + statusbar: this.statusbarPart, + }, + this.quickOpen, + this.quickInput, + this.notificationsCenter, + this.notificationsToasts + ); + } } private renderWorkbench(): void { @@ -1078,9 +1138,11 @@ export class Workbench extends Disposable implements IPartService { part.id = id; part.setAttribute('role', role); - // Insert all workbench parts at the beginning. Issue #52531 - // This is primarily for the title bar to allow overriding -webkit-app-region - this.workbench.insertBefore(part, this.workbench.lastChild); + if (!this.configurationService.getValue('workbench.useExperimentalGridLayout')) { + // Insert all workbench parts at the beginning. Issue #52531 + // This is primarily for the title bar to allow overriding -webkit-app-region + this.workbench.insertBefore(part, this.workbench.lastChild); + } return part; } @@ -1218,7 +1280,12 @@ export class Workbench extends Disposable implements IPartService { getTitleBarOffset(): number { let offset = 0; if (this.isVisible(Parts.TITLEBAR_PART)) { - offset = this.workbenchLayout.partLayoutInfo.titlebar.height; + if (this.workbenchGrid instanceof Grid) { + offset = this.gridHasView(this.titlebarPartView) ? this.workbenchGrid.getViewSize2(this.titlebarPartView).height : 0; + } else { + offset = this.workbenchGrid.partLayoutInfo.titlebar.height; + } + if (isMacintosh || this.menubarVisibility === 'hidden') { offset /= browser.getZoomFactor(); } @@ -1316,11 +1383,130 @@ export class Workbench extends Disposable implements IPartService { } } + private gridHasView(view: View): boolean { + if (!(this.workbenchGrid instanceof Grid)) { + return false; + } + + try { + this.workbenchGrid.getViewSize(view); + return true; + } catch { + return false; + } + } + + private updateGrid(): void { + if (!(this.workbenchGrid instanceof Grid)) { + return; + } + + let panelInGrid = this.gridHasView(this.panelPartView); + let sidebarInGrid = this.gridHasView(this.sidebarPartView); + let activityBarInGrid = this.gridHasView(this.activitybarPartView); + let statusBarInGrid = this.gridHasView(this.statusbarPartView); + let titlebarInGrid = this.gridHasView(this.titlebarPartView); + + // Add parts to grid + if (!statusBarInGrid) { + this.workbenchGrid.addView(this.statusbarPartView, Sizing.Split, this.editorPartView, Direction.Down); + statusBarInGrid = true; + } + + if (!titlebarInGrid && this.useCustomTitleBarStyle()) { + this.workbenchGrid.addView(this.titlebarPartView, Sizing.Split, this.editorPartView, Direction.Up); + titlebarInGrid = true; + } + + if (!activityBarInGrid) { + this.workbenchGrid.addView(this.activitybarPartView, Sizing.Split, panelInGrid && this.sideBarPosition === this.panelPosition ? this.panelPartView : this.editorPartView, this.sideBarPosition === Position.RIGHT ? Direction.Right : Direction.Left); + activityBarInGrid = true; + } + + if (!sidebarInGrid) { + this.workbenchGrid.addView(this.sidebarPartView, this.uiState.lastSidebarDimension !== undefined ? this.uiState.lastSidebarDimension : Sizing.Split, this.activitybarPartView, this.sideBarPosition === Position.LEFT ? Direction.Right : Direction.Left); + sidebarInGrid = true; + } + + if (!panelInGrid) { + this.workbenchGrid.addView(this.panelPartView, this.getLastPanelDimension(this.panelPosition) !== undefined ? this.getLastPanelDimension(this.panelPosition) : Sizing.Split, this.editorPartView, this.panelPosition === Position.BOTTOM ? Direction.Down : Direction.Right); + panelInGrid = true; + } + + // Hide parts + if (this.panelHidden) { + this.panelPartView.hide(); + } + + if (this.statusBarHidden) { + this.statusbarPartView.hide(); + } + + if (!this.isVisible(Parts.TITLEBAR_PART)) { + this.titlebarPartView.hide(); + } + + if (this.activityBarHidden) { + this.activitybarPartView.hide(); + } + + if (this.sideBarHidden) { + this.sidebarPartView.hide(); + } + + if (this.editorHidden) { + this.editorPartView.hide(); + } + + // Show visible parts + if (!this.editorHidden) { + this.editorPartView.show(); + } + + if (!this.statusBarHidden) { + this.statusbarPartView.show(); + } + + if (this.isVisible(Parts.TITLEBAR_PART)) { + this.titlebarPartView.show(); + } + + if (!this.activityBarHidden) { + this.activitybarPartView.show(); + } + + if (!this.sideBarHidden) { + this.sidebarPartView.show(); + } + + if (!this.panelHidden) { + this.panelPartView.show(); + } + } + layout(options?: ILayoutOptions): void { this.contextViewService.layout(); if (this.workbenchStarted && !this.workbenchShutdown) { - this.workbenchLayout.layout(options); + if (this.workbenchGrid instanceof Grid) { + const dimensions = DOM.getClientArea(this.container); + DOM.position(this.workbench, 0, 0, 0, 0, 'relative'); + DOM.size(this.workbench, dimensions.width, dimensions.height); + + // Layout the grid + this.workbenchGrid.layout(dimensions.width, dimensions.height); + + // Layout non-view ui components + this.quickInput.layout(dimensions); + this.quickOpen.layout(dimensions); + this.notificationsCenter.layout(dimensions); + this.notificationsToasts.layout(dimensions); + + // Update grid view membership + this.updateGrid(); + } else { + this.workbenchGrid.layout(options); + } } } @@ -1347,11 +1533,19 @@ export class Workbench extends Disposable implements IPartService { } resizePart(part: Parts, sizeChange: number): void { + let view: View; switch (part) { case Parts.SIDEBAR_PART: + view = this.sidebarPartView; case Parts.PANEL_PART: + view = this.panelPartView; case Parts.EDITOR_PART: - this.workbenchLayout.resizePart(part, sizeChange); + view = this.editorPartView; + if (this.workbenchGrid instanceof Grid) { + this.workbenchGrid.resizeView(view, this.workbenchGrid.getViewSize(view) + sizeChange); + } else { + this.workbenchGrid.resizePart(part, sizeChange); + } break; default: return; // Cannot resize other parts @@ -1363,7 +1557,28 @@ export class Workbench extends Disposable implements IPartService { // Layout if (!skipLayout) { - this.workbenchLayout.layout(); + if (this.workbenchGrid instanceof Grid) { + this.layout(); + } else { + this.workbenchGrid.layout(); + } + } + } + + setEditorHidden(hidden: boolean, skipLayout?: boolean): void { + if (!(this.workbenchGrid instanceof Grid)) { + return; + } + + this.editorHidden = hidden; + + // The editor and the panel cannot be hidden at the same time + if (this.editorHidden && this.panelHidden) { + this.setPanelHidden(false, true); + } + + if (!skipLayout) { + this.layout(); } } @@ -1413,7 +1628,11 @@ export class Workbench extends Disposable implements IPartService { // Layout if (!skipLayout) { - this.workbenchLayout.layout(); + if (this.workbenchGrid instanceof Grid) { + this.layout(); + } else { + this.workbenchGrid.layout(); + } } } @@ -1449,18 +1668,39 @@ export class Workbench extends Disposable implements IPartService { this.storageService.remove(Workbench.panelHiddenStorageKey, StorageScope.WORKSPACE); } + // The editor and panel cannot be hiddne at the same time + if (hidden && this.editorHidden) { + this.setEditorHidden(false, true); + } + // Layout if (!skipLayout) { - this.workbenchLayout.layout(); + if (this.workbenchGrid instanceof Grid) { + this.layout(); + } else { + this.workbenchGrid.layout(); + } } } toggleMaximizedPanel(): void { - this.workbenchLayout.layout({ toggleMaximizedPanel: true, source: Parts.PANEL_PART }); + if (this.workbenchGrid instanceof Grid) { + this.workbenchGrid.maximizeViewSize(this.panelPartView); + } else { + this.workbenchGrid.layout({ toggleMaximizedPanel: true, source: Parts.PANEL_PART }); + } } isPanelMaximized(): boolean { - return this.workbenchLayout.isPanelMaximized(); + if (this.workbenchGrid instanceof Grid) { + try { + return this.workbenchGrid.getViewSize2(this.panelPartView).height === this.panelPart.maximumHeight; + } catch (e) { + return false; + } + } else { + return this.workbenchGrid.isPanelMaximized(); + } } getSideBarPosition(): Position { @@ -1468,6 +1708,8 @@ export class Workbench extends Disposable implements IPartService { } setSideBarPosition(position: Position): void { + const wasHidden = this.sideBarHidden; + if (this.sideBarHidden) { this.setSideBarHidden(false, true /* Skip Layout */); } @@ -1487,15 +1729,39 @@ export class Workbench extends Disposable implements IPartService { this.sidebarPart.updateStyles(); // Layout - this.workbenchLayout.layout(); + if (this.workbenchGrid instanceof Grid) { + + if (!wasHidden) { + this.uiState.lastSidebarDimension = this.workbenchGrid.getViewSize(this.sidebarPartView); + this.workbenchGrid.removeView(this.sidebarPartView); + } + + if (!this.activityBarHidden) { + this.workbenchGrid.removeView(this.activitybarPartView); + } + + if (!this.panelHidden && this.panelPosition === Position.BOTTOM) { + this.workbenchGrid.removeView(this.panelPartView); + } + + this.layout(); + } else { + this.workbenchGrid.layout(); + } } setMenubarVisibility(visibility: MenuBarVisibility, skipLayout: boolean): void { if (this.menubarVisibility !== visibility) { this.menubarVisibility = visibility; + // Layout if (!skipLayout) { - this.workbenchLayout.layout(); + if (this.workbenchGrid instanceof Grid) { + const dimensions = DOM.getClientArea(this.container); + this.workbenchGrid.layout(dimensions.width, dimensions.height); + } else { + this.workbenchGrid.layout(); + } } } } @@ -1509,8 +1775,12 @@ export class Workbench extends Disposable implements IPartService { } setPanelPosition(position: Position): void { + const wasHidden = this.panelHidden; + if (this.panelHidden) { this.setPanelHidden(false, true /* Skip Layout */); + } else { + this.saveLastPanelDimension(); } const newPositionValue = (position === Position.BOTTOM) ? 'bottom' : 'right'; @@ -1526,8 +1796,15 @@ export class Workbench extends Disposable implements IPartService { this.panelPart.updateStyles(); // Layout - this.workbenchLayout.layout(); - } + if (this.workbenchGrid instanceof Grid) { + if (!wasHidden) { + this.saveLastPanelDimension(); + this.workbenchGrid.removeView(this.panelPartView); + } - //#endregion -} + this.layout(); + } else { + this.workbenchGrid.layout(); + } + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/codeEditor/codeEditor.contribution.ts b/src/vs/workbench/parts/codeEditor/codeEditor.contribution.ts deleted file mode 100644 index 871d6ab2d6b..00000000000 --- a/src/vs/workbench/parts/codeEditor/codeEditor.contribution.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 './browser/menuPreventer'; -import './electron-browser/accessibility'; -import './electron-browser/inspectKeybindings'; -import './electron-browser/largeFileOptimizations'; -import './electron-browser/selectionClipboard'; -import './electron-browser/sleepResumeRepaintMinimap'; -import './electron-browser/textMate/inspectTMScopes'; -import './electron-browser/toggleMinimap'; -import './electron-browser/toggleMultiCursorModifier'; -import './electron-browser/toggleRenderControlCharacter'; -import './electron-browser/toggleRenderWhitespace'; -import './electron-browser/toggleWordWrap'; -import './electron-browser/workbenchReferenceSearch'; \ No newline at end of file diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/codeEditor.contribution.ts b/src/vs/workbench/parts/codeEditor/electron-browser/codeEditor.contribution.ts new file mode 100644 index 00000000000..2f9faa53af6 --- /dev/null +++ b/src/vs/workbench/parts/codeEditor/electron-browser/codeEditor.contribution.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import '../browser/menuPreventer'; +import './accessibility'; +import './inspectKeybindings'; +import './largeFileOptimizations'; +import './selectionClipboard'; +import './sleepResumeRepaintMinimap'; +import './textMate/inspectTMScopes'; +import './toggleMinimap'; +import './toggleMultiCursorModifier'; +import './toggleRenderControlCharacter'; +import './toggleRenderWhitespace'; +import './toggleWordWrap'; +import './workbenchReferenceSearch'; \ No newline at end of file diff --git a/src/vs/workbench/parts/comments/electron-browser/commentGlyphWidget.ts b/src/vs/workbench/parts/comments/electron-browser/commentGlyphWidget.ts index 35ce6e139da..a268f68a24c 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentGlyphWidget.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentGlyphWidget.ts @@ -23,9 +23,8 @@ export class CommentGlyphWidget { constructor(editor: ICodeEditor, lineNumber: number) { this._commentsOptions = this.createDecorationOptions(); - this._lineNumber = lineNumber; this._editor = editor; - this.update(); + this.setLineNumber(lineNumber); } private createDecorationOptions(): ModelDecorationOptions { @@ -41,11 +40,12 @@ export class CommentGlyphWidget { return ModelDecorationOptions.createDynamic(decorationOptions); } - update() { + setLineNumber(lineNumber: number): void { + this._lineNumber = lineNumber; let commentsDecorations = [{ range: { - startLineNumber: this._lineNumber, startColumn: 1, - endLineNumber: this._lineNumber, endColumn: 1 + startLineNumber: lineNumber, startColumn: 1, + endLineNumber: lineNumber, endColumn: 1 }, options: this._commentsOptions }]; @@ -53,15 +53,14 @@ export class CommentGlyphWidget { this.commentsDecorations = this._editor.deltaDecorations(this.commentsDecorations, commentsDecorations); } - setLineNumber(lineNumber: number): void { - this._lineNumber = lineNumber; - this.update(); - } - getPosition(): IContentWidgetPosition { + const range = this._editor.hasModel() && this.commentsDecorations && this.commentsDecorations.length + ? this._editor.getModel().getDecorationRange(this.commentsDecorations[0]) + : null; + return { position: { - lineNumber: this._lineNumber, + lineNumber: range ? range.startLineNumber : this._lineNumber, column: 1 }, preference: [ContentWidgetPositionPreference.EXACT] @@ -73,4 +72,4 @@ export class CommentGlyphWidget { this._editor.deltaDecorations(this.commentsDecorations, []); } } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/comments/electron-browser/commentNode.ts b/src/vs/workbench/parts/comments/electron-browser/commentNode.ts index 21077bd08aa..5d1c0b3b434 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentNode.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentNode.ts @@ -7,9 +7,9 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import * as modes from 'vs/editor/common/modes'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionsOrientation, ActionItem, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button } from 'vs/base/browser/ui/button/button'; -import { Action } from 'vs/base/common/actions'; +import { Action, IActionRunner } from 'vs/base/common/actions'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; @@ -30,6 +30,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { assign } from 'vs/base/common/objects'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; const UPDATE_COMMENT_LABEL = nls.localize('label.updateComment', "Update comment"); const UPDATE_IN_PROGRESS_LABEL = nls.localize('label.updatingComment', "Updating comment..."); @@ -42,6 +44,9 @@ export class CommentNode extends Disposable { private _editAction: Action; private _commentEditContainer: HTMLElement; + private _commentDetailsContainer: HTMLElement; + private _reactionsActionBar?: ActionBar; + private _actionsContainer?: HTMLElement; private _commentEditor: SimpleCommentEditor; private _commentEditorModel: ITextModel; private _updateCommentButton: Button; @@ -49,6 +54,9 @@ export class CommentNode extends Disposable { private _isPendingLabel: HTMLElement; private _deleteAction: Action; + protected actionRunner?: IActionRunner; + protected toolbar: ToolBar; + private _onDidDelete = new Emitter(); public get domNode(): HTMLElement { @@ -66,7 +74,8 @@ export class CommentNode extends Disposable { private modelService: IModelService, private modeService: IModeService, private dialogService: IDialogService, - private notificationService: INotificationService + private notificationService: INotificationService, + private contextMenuService: IContextMenuService ) { super(); @@ -78,14 +87,18 @@ export class CommentNode extends Disposable { img.src = comment.userIconPath.toString(); img.onerror = _ => img.remove(); } - const commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents')); + this._commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents')); - this.createHeader(commentDetailsContainer); + this.createHeader(this._commentDetailsContainer); - this._body = dom.append(commentDetailsContainer, dom.$('div.comment-body')); + this._body = dom.append(this._commentDetailsContainer, dom.$('div.comment-body')); this._md = this.markdownRenderer.render(comment.body).element; this._body.appendChild(this._md); + if (this.comment.commentReactions && this.comment.commentReactions.length) { + this.createReactions(this._commentDetailsContainer); + } + this._domNode.setAttribute('aria-label', `${comment.userName}, ${comment.body.value}`); this._domNode.setAttribute('role', 'treeitem'); this._clearTimeout = null; @@ -119,14 +132,81 @@ export class CommentNode extends Disposable { if (actions.length) { const actionsContainer = dom.append(header, dom.$('.comment-actions.hidden')); - const actionBar = new ActionBar(actionsContainer, {}); - this._toDispose.push(actionBar); + + this.toolbar = new ToolBar(actionsContainer, this.contextMenuService, { + actionItemProvider: action => this.actionItemProvider(action as Action), + orientation: ActionsOrientation.HORIZONTAL + }); + this.registerActionBarListeners(actionsContainer); - actions.forEach(action => actionBar.push(action, { label: false, icon: true })); + let reactionActions = []; + let reactionGroup = this.commentService.getReactionGroup(this.owner); + if (reactionGroup && reactionGroup.length) { + reactionActions = reactionGroup.map((reaction) => { + return new Action(`reaction.command.${reaction.label}`, `${reaction.label}`, '', true, async () => { + try { + await this.commentService.addReaction(this.owner, this.resource, this.comment, reaction); + } catch (e) { + const error = e.message + ? nls.localize('commentAddReactionError', "Deleting the comment reaction failed: {0}.", e.message) + : nls.localize('commentAddReactionDefaultError', "Deleting the comment reaction failed"); + this.notificationService.error(error); + } + }); + }); + } + + this.toolbar.setActions(actions, reactionActions)(); + this._toDispose.push(this.toolbar); } } + actionItemProvider(action: Action) { + let options = {}; + if (action.id === 'comment.delete' || action.id === 'comment.edit') { + options = { label: false, icon: true }; + } else { + options = { label: true, icon: true }; + } + + let item = new ActionItem({}, action, options); + return item; + } + + private createReactions(commentDetailsContainer: HTMLElement): void { + this._actionsContainer = dom.append(commentDetailsContainer, dom.$('div.comment-reactions')); + this._reactionsActionBar = new ActionBar(this._actionsContainer, {}); + this._toDispose.push(this._reactionsActionBar); + + let reactionActions = this.comment.commentReactions.map(reaction => { + return new Action(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted ? 'active' : '', true, async () => { + try { + if (reaction.hasReacted) { + await this.commentService.deleteReaction(this.owner, this.resource, this.comment, reaction); + } else { + await this.commentService.addReaction(this.owner, this.resource, this.comment, reaction); + } + } catch (e) { + let error: string; + + if (reaction.hasReacted) { + error = e.message + ? nls.localize('commentDeleteReactionError', "Deleting the comment reaction failed: {0}.", e.message) + : nls.localize('commentDeleteReactionDefaultError', "Deleting the comment reaction failed"); + } else { + error = e.message + ? nls.localize('commentAddReactionError', "Deleting the comment reaction failed: {0}.", e.message) + : nls.localize('commentAddReactionDefaultError', "Deleting the comment reaction failed"); + } + this.notificationService.error(error); + } + }); + }); + + reactionActions.forEach(action => this._reactionsActionBar.push(action, { label: true, icon: true })); + } + private createCommentEditor(): void { const container = dom.append(this._commentEditContainer, dom.$('.edit-textarea')); this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, container, SimpleCommentEditor.getEditorOptions()); @@ -259,7 +339,7 @@ export class CommentNode extends Disposable { actionsContainer.classList.remove('hidden'); })); - this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseleave', (e: MouseEvent) => { + this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseleave', () => { if (!this._domNode.contains(document.activeElement)) { actionsContainer.classList.add('hidden'); } @@ -277,6 +357,8 @@ export class CommentNode extends Disposable { } update(newComment: modes.Comment) { + this.comment = newComment; + if (newComment.body !== this.comment.body) { this._body.removeChild(this._md); this._md = this.markdownRenderer.render(newComment.body).element; @@ -289,7 +371,18 @@ export class CommentNode extends Disposable { this._isPendingLabel.innerText = ''; } - this.comment = newComment; + // update comment reactions + if (this._actionsContainer) { + this._actionsContainer.remove(); + } + + if (this._reactionsActionBar) { + this._reactionsActionBar.clear(); + } + + if (this.comment.commentReactions && this.comment.commentReactions.length) { + this.createReactions(this._commentDetailsContainer); + } } focus() { diff --git a/src/vs/workbench/parts/comments/electron-browser/commentService.ts b/src/vs/workbench/parts/comments/electron-browser/commentService.ts index 0a806cc0332..7173a1b02db 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentService.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo, Comment } from 'vs/editor/common/modes'; +import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo, Comment, CommentReaction } from 'vs/editor/common/modes'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -55,6 +55,9 @@ export interface ICommentService { getStartDraftLabel(owner: string): string; getDeleteDraftLabel(owner: string): string; getFinishDraftLabel(owner: string): string; + addReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise; + deleteReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise; + getReactionGroup(owner: string): CommentReaction[]; } export class CommentService extends Disposable implements ICommentService { @@ -178,6 +181,36 @@ export class CommentService extends Disposable implements ICommentService { } } + async addReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise { + const commentProvider = this._commentProviders.get(owner); + + if (commentProvider && commentProvider.addReaction) { + return commentProvider.addReaction(resource, comment, reaction, CancellationToken.None); + } else { + throw new Error('Not supported'); + } + } + + async deleteReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise { + const commentProvider = this._commentProviders.get(owner); + + if (commentProvider && commentProvider.deleteReaction) { + return commentProvider.deleteReaction(resource, comment, reaction, CancellationToken.None); + } else { + throw new Error('Not supported'); + } + } + + getReactionGroup(owner: string): CommentReaction[] { + const commentProvider = this._commentProviders.get(owner); + + if (commentProvider) { + return commentProvider.reactionGroup; + } + + return null; + } + getStartDraftLabel(owner: string): string | null { const commentProvider = this._commentProviders.get(owner); diff --git a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts index b009344ee58..2af96523bbe 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts @@ -39,6 +39,7 @@ import { CommentNode } from 'vs/workbench/parts/comments/electron-browser/commen import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ITextModel } from 'vs/editor/common/model'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-x'; @@ -97,12 +98,13 @@ export class ReviewZoneWidget extends ZoneWidget { private openerService: IOpenerService, private dialogService: IDialogService, private notificationService: INotificationService, + private contextMenuService: IContextMenuService, editor: ICodeEditor, owner: string, commentThread: modes.CommentThread, pendingComment: string, draftMode: modes.DraftMode, - options: IOptions = {} + options: IOptions = { keepEditorSelection: true } ) { super(editor, options); this._resizeObserver = null; @@ -240,7 +242,7 @@ export class ReviewZoneWidget extends ZoneWidget { // del removed elements for (let i = commentElementsToDel.length - 1; i >= 0; i--) { - this._commentElements.splice(commentElementsToDelIndex[i]); + this._commentElements.splice(commentElementsToDelIndex[i], 1); this._commentsElement.removeChild(commentElementsToDel[i].domNode); } @@ -269,6 +271,16 @@ export class ReviewZoneWidget extends ZoneWidget { this._commentThread = commentThread; this._commentElements = newCommentNodeList; this.createThreadLabel(); + + // Move comment glyph widget and show position if the line has changed. + const lineNumber = this._commentThread.range.startLineNumber; + if (this._commentGlyph.getPosition().position.lineNumber !== lineNumber) { + this._commentGlyph.setLineNumber(lineNumber); + } + + if (!this._isCollapsed) { + this.show({ lineNumber, column: 1 }, 2); + } } updateDraftMode(draftMode: modes.DraftMode) { @@ -388,7 +400,7 @@ export class ReviewZoneWidget extends ZoneWidget { private createCommentWidgetActions(container: HTMLElement, model: ITextModel) { const button = new Button(container); - this._disposables.push(attachButtonStyler(button, this.themeService)); + this._localToDispose.push(attachButtonStyler(button, this.themeService)); button.label = 'Add comment'; button.enabled = model.getValueLength() > 0; @@ -491,7 +503,8 @@ export class ReviewZoneWidget extends ZoneWidget { this.modelService, this.modeService, this.dialogService, - this.notificationService); + this.notificationService, + this.contextMenuService); this._disposables.push(newCommentNode); this._disposables.push(newCommentNode.onDidDelete(deletedNode => { diff --git a/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts b/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts index c6f1b95096c..f6cbca33e7a 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts @@ -35,6 +35,8 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { overviewRulerCommentingRangeForeground } from 'vs/workbench/parts/comments/electron-browser/commentGlyphWidget'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme'; export const ctxReviewPanelVisible = new RawContextKey('reviewPanelVisible', false); @@ -175,7 +177,8 @@ export class ReviewController implements IEditorContribution { @IModelService private readonly modelService: IModelService, @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IOpenerService private readonly openerService: IOpenerService, - @IDialogService private readonly dialogService: IDialogService + @IDialogService private readonly dialogService: IDialogService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { this.editor = editor; this.globalToDispose = []; @@ -393,7 +396,7 @@ export class ReviewController implements IEditorContribution { } }); added.forEach(thread => { - let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.dialogService, this.notificationService, this.editor, e.owner, thread, null, draftMode, {}); + let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.dialogService, this.notificationService, this.contextMenuService, this.editor, e.owner, thread, null, draftMode); zoneWidget.display(thread.range.startLineNumber); this._commentWidgets.push(zoneWidget); this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread); @@ -412,7 +415,7 @@ export class ReviewController implements IEditorContribution { // add new comment this._reviewPanelVisible.set(true); - this._newCommentWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.dialogService, this.notificationService, this.editor, ownerId, { + this._newCommentWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.dialogService, this.notificationService, this.contextMenuService, this.editor, ownerId, { extensionId: extensionId, threadId: null, resource: null, @@ -425,7 +428,7 @@ export class ReviewController implements IEditorContribution { }, reply: replyCommand, collapsibleState: CommentThreadCollapsibleState.Expanded, - }, pendingComment, draftMode, {}); + }, pendingComment, draftMode); this.localToDispose.push(this._newCommentWidget.onDidClose(e => { this.clearNewCommentWidget(); @@ -570,7 +573,7 @@ export class ReviewController implements IEditorContribution { thread.collapsibleState = modes.CommentThreadCollapsibleState.Expanded; } - let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.dialogService, this.notificationService, this.editor, info.owner, thread, pendingComment, info.draftMode, {}); + let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.dialogService, this.notificationService, this.contextMenuService, this.editor, info.owner, thread, pendingComment, info.draftMode); zoneWidget.display(thread.range.startLineNumber); this._commentWidgets.push(zoneWidget); }); @@ -741,4 +744,16 @@ registerThemingParticipant((theme, collector) => { } `); } + + const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); + if (statusBarItemHoverBackground) { + collector.addRule(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label:hover { background-color: ${statusBarItemHoverBackground}; border: 1px solid grey; + border-radius: 3px; }`); + } + + const statusBarItemActiveBackground = theme.getColor(STATUS_BAR_ITEM_ACTIVE_BACKGROUND); + if (statusBarItemActiveBackground) { + collector.addRule(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label:active { background-color: ${statusBarItemActiveBackground}; border: 1px solid grey; + border-radius: 3px;}`); + } }); diff --git a/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts b/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts index 43a7d4b5d85..307216ce03d 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts @@ -20,7 +20,7 @@ import { CommentsDataFilter, CommentsDataSource, CommentsModelRenderer } from 'v import { ICommentService, IWorkspaceCommentThreadsEvent } from 'vs/workbench/parts/comments/electron-browser/commentService'; import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { textLinkForeground, textLinkActiveForeground, focusBorder } from 'vs/platform/theme/common/colorRegistry'; +import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ResourceLabels } from 'vs/workbench/browser/labels'; @@ -97,6 +97,11 @@ export class CommentsPanel extends Panel { content.push(`.comments-panel .commenst-panel-container a:focus { outline-color: ${focusColor}; }`); } + const codeTextForegroundColor = theme.getColor(textPreformatForeground); + if (codeTextForegroundColor) { + content.push(`.comments-panel .comments-panel-container .text code { color: ${codeTextForegroundColor}; }`); + } + styleElement.innerHTML = content.join('\n'); } diff --git a/src/vs/workbench/parts/comments/electron-browser/media/panel.css b/src/vs/workbench/parts/comments/electron-browser/media/panel.css index 3c2b37bcf3f..43f2d98ff6b 100644 --- a/src/vs/workbench/parts/comments/electron-browser/media/panel.css +++ b/src/vs/workbench/parts/comments/electron-browser/media/panel.css @@ -41,6 +41,10 @@ overflow: hidden; } +.comments-panel .comments-panel-container .tree-container .comment-container .text code { + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; +} + .comments-panel .comments-panel-container .message-box-container { line-height: 22px; padding-left: 20px; diff --git a/src/vs/workbench/parts/comments/electron-browser/media/review.css b/src/vs/workbench/parts/comments/electron-browser/media/review.css index eaecf20ed26..b8cf5584e59 100644 --- a/src/vs/workbench/parts/comments/electron-browser/media/review.css +++ b/src/vs/workbench/parts/comments/electron-browser/media/review.css @@ -56,6 +56,13 @@ line-height: 18px; } +.monaco-editor .review-widget .body .review-comment .comment-title .monaco-dropdown .toolbar-toggle-more { + width: 16px; + height: 18px; + line-height: 18px; + vertical-align: middle; +} + .monaco-editor .review-widget .body .comment-body blockquote { margin: 0 7px 0 5px; padding: 0 16px 0 10px; @@ -78,6 +85,15 @@ border-style: none; } + +.monaco-editor .review-widget .body .monaco-text-button { + margin: 0 7px 0 0; + width: 30px; + background-color: transparent; + border: 1px solid grey; + border-radius: 3px; +} + .monaco-editor .review-widget .body .review-comment .review-comment-contents { padding-left: 20px; user-select: text; @@ -111,6 +127,32 @@ padding-top: 4px; } +.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions { + margin-top: 8px; + min-height: 25px; +} + +.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .monaco-action-bar .actions-container { + justify-content: flex-start; +} + +.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label { + padding: 2px 5px 2px 5px; + white-space: pre; + text-align: center; + font-size: 14px; + margin: 4px; +} + +.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.active { + border: 1px solid grey; + border-radius: 3px; +} + +.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.disabled { + opacity: 0.6; +} + .monaco-editor.vs-dark .review-widget .body span.created_at { color: #e0e0e0; } diff --git a/src/vs/workbench/parts/debug/browser/breakpointsView.ts b/src/vs/workbench/parts/debug/browser/breakpointsView.ts index 1c572502b27..7361a91dbb4 100644 --- a/src/vs/workbench/parts/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/parts/debug/browser/breakpointsView.ts @@ -134,9 +134,9 @@ export class BreakpointsView extends ViewletPanel { } } - protected layoutBody(size: number): void { + protected layoutBody(height: number, width: number): void { if (this.list) { - this.list.layout(size); + this.list.layout(height, width); } } diff --git a/src/vs/workbench/parts/debug/browser/loadedScriptsView.ts b/src/vs/workbench/parts/debug/browser/loadedScriptsView.ts index c8584cb4ef3..fabd77cd8c0 100644 --- a/src/vs/workbench/parts/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/parts/debug/browser/loadedScriptsView.ts @@ -500,8 +500,8 @@ export class LoadedScriptsView extends ViewletPanel { })); } - layoutBody(size: number): void { - this.tree.layout(size); + layoutBody(height: number, width: number): void { + this.tree.layout(height, width); } dispose(): void { diff --git a/src/vs/workbench/parts/debug/browser/media/debug.contribution.css b/src/vs/workbench/parts/debug/browser/media/debug.contribution.css index 330f9e8a0b9..7a5e0f3cfa2 100644 --- a/src/vs/workbench/parts/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/parts/debug/browser/media/debug.contribution.css @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ /* Activity Bar */ -.monaco-workbench > .activitybar .monaco-action-bar .action-label.debug { +.monaco-workbench .activitybar .monaco-action-bar .action-label.debug { -webkit-mask: url('debug-dark.svg') no-repeat 50% 50%; } @@ -112,7 +112,7 @@ /* Debug status */ /* A very precise css rule to overwrite the display set in statusbar.css */ -.monaco-workbench > .part.statusbar > .statusbar-item > .debug-statusbar-item > a { +.monaco-workbench .part.statusbar > .statusbar-item > .debug-statusbar-item > a { display: flex; padding: 0 5px 0 5px; } diff --git a/src/vs/workbench/parts/debug/browser/media/debugHover.css b/src/vs/workbench/parts/debug/browser/media/debugHover.css index 2f1115e52df..2813a7377e0 100644 --- a/src/vs/workbench/parts/debug/browser/media/debugHover.css +++ b/src/vs/workbench/parts/debug/browser/media/debugHover.css @@ -36,7 +36,6 @@ .monaco-editor .debug-hover-widget .debug-hover-tree .monaco-list-row .monaco-tl-contents { user-select: text; - white-space: pre; } /* Disable tree highlight in debug hover tree. */ @@ -60,6 +59,10 @@ max-height: 500px; } +.monaco-editor .debug-hover-widget .monaco-tl-contents .value { + white-space: nowrap; +} + .monaco-editor .debug-hover-widget .error { color: #E51400; } diff --git a/src/vs/workbench/parts/debug/browser/media/debugViewlet.css b/src/vs/workbench/parts/debug/browser/media/debugViewlet.css index 38b36067f54..7cf772fb7b0 100644 --- a/src/vs/workbench/parts/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/parts/debug/browser/media/debugViewlet.css @@ -54,7 +54,7 @@ background: url('repl-inverse.svg') center center no-repeat; } -.monaco-workbench > .part > .title > .title-actions .start-debug-action-item { +.monaco-workbench .part > .title > .title-actions .start-debug-action-item { display: flex; align-items: center; font-size: 11px; @@ -68,7 +68,7 @@ border-radius: 4px; } -.monaco-workbench > .part > .title > .title-actions .start-debug-action-item .icon { +.monaco-workbench .part > .title > .title-actions .start-debug-action-item .icon { height: 20px; width: 20px; background: url('start.svg') no-repeat; @@ -78,8 +78,8 @@ transition: transform 50ms ease; } -.vs-dark .monaco-workbench > .part > .title > .title-actions .start-debug-action-item .icon, -.hc-black .monaco-workbench > .part > .title > .title-actions .start-debug-action-item .icon { +.vs-dark .monaco-workbench .part > .title > .title-actions .start-debug-action-item .icon, +.hc-black .monaco-workbench .part > .title > .title-actions .start-debug-action-item .icon { background-image: url('start-inverse.svg'); } @@ -95,7 +95,7 @@ cursor: initial; } -.monaco-workbench > .part > .title > .title-actions .start-debug-action-item .icon.active { +.monaco-workbench .part > .title > .title-actions .start-debug-action-item .icon.active { transform: scale(1.272019649, 1.272019649); } diff --git a/src/vs/workbench/parts/debug/browser/statusbarColorProvider.ts b/src/vs/workbench/parts/debug/browser/statusbarColorProvider.ts index f3cdc5b0192..43591298a91 100644 --- a/src/vs/workbench/parts/debug/browser/statusbarColorProvider.ts +++ b/src/vs/workbench/parts/debug/browser/statusbarColorProvider.ts @@ -79,7 +79,7 @@ export class StatusBarColorProvider extends Themable implements IWorkbenchContri this.styleElement = createStyleSheet(container); } - this.styleElement.innerHTML = `.monaco-workbench > .part.statusbar > .statusbar-item.has-beak:before { border-bottom-color: ${backgroundColor} !important; }`; + this.styleElement.innerHTML = `.monaco-workbench .part.statusbar > .statusbar-item.has-beak:before { border-bottom-color: ${backgroundColor} !important; }`; } private getColorKey(noFolderColor: string, debuggingColor: string, normalColor: string): string { @@ -115,6 +115,6 @@ export function isStatusbarInDebugMode(debugService: IDebugService): boolean { registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const statusBarItemDebuggingForeground = theme.getColor(STATUS_BAR_DEBUGGING_FOREGROUND); if (statusBarItemDebuggingForeground) { - collector.addRule(`.monaco-workbench > .part.statusbar.debugging > .statusbar-item .mask-icon { background-color: ${statusBarItemDebuggingForeground} !important; }`); + collector.addRule(`.monaco-workbench .part.statusbar.debugging > .statusbar-item .mask-icon { background-color: ${statusBarItemDebuggingForeground} !important; }`); } }); diff --git a/src/vs/workbench/parts/debug/common/debugUtils.ts b/src/vs/workbench/parts/debug/common/debugUtils.ts index 88b3c6e2251..0d00748fd61 100644 --- a/src/vs/workbench/parts/debug/common/debugUtils.ts +++ b/src/vs/workbench/parts/debug/common/debugUtils.ts @@ -79,78 +79,84 @@ export function isUri(s: string) { return s && s.match(_schemePattern); } -export function stringToUri(source: DebugProtocol.Source): void { - if (typeof source.path === 'string') { - if (isUri(source.path)) { - (source).path = uri.parse(source.path); +function stringToUri(path: string): string { + if (typeof path === 'string') { + if (isUri(path)) { + return uri.parse(path); } else { // assume path - if (isAbsolute_posix(source.path) || isAbsolute_win32(source.path)) { - (source).path = uri.file(source.path); + if (isAbsolute_posix(path) || isAbsolute_win32(path)) { + return uri.file(path); } else { // leave relative path as is } } } + return path; } -export function uriToString(source: DebugProtocol.Source): void { - if (typeof source.path === 'object') { - const u = uri.revive(source.path); +function uriToString(path: string): string { + if (typeof path === 'object') { + const u = uri.revive(path); if (u.scheme === 'file') { - source.path = u.fsPath; + return u.fsPath; } else { - source.path = u.toString(); + return u.toString(); } } + return path; } // path hooks helpers -export function convertToDAPaths(message: DebugProtocol.ProtocolMessage, fixSourcePaths: (source: DebugProtocol.Source) => void): DebugProtocol.ProtocolMessage { +interface PathContainer { + path?: string; +} + +export function convertToDAPaths(message: DebugProtocol.ProtocolMessage, toUri: boolean): DebugProtocol.ProtocolMessage { + + const fixPath = toUri ? stringToUri : uriToString; // since we modify Source.paths in the message in place, we need to make a copy of it (see #61129) const msg = deepClone(message); - convertPaths(msg, (toDA: boolean, source: DebugProtocol.Source | undefined) => { + convertPaths(msg, (toDA: boolean, source: PathContainer | undefined) => { if (toDA && source) { - fixSourcePaths(source); + source.path = fixPath(source.path); } }); return msg; } -export function convertToVSCPaths(message: DebugProtocol.ProtocolMessage, fixSourcePaths: (source: DebugProtocol.Source) => void): DebugProtocol.ProtocolMessage { +export function convertToVSCPaths(message: DebugProtocol.ProtocolMessage, toUri: boolean): DebugProtocol.ProtocolMessage { + + const fixPath = toUri ? stringToUri : uriToString; // since we modify Source.paths in the message in place, we need to make a copy of it (see #61129) const msg = deepClone(message); - convertPaths(msg, (toDA: boolean, source: DebugProtocol.Source | undefined) => { + convertPaths(msg, (toDA: boolean, source: PathContainer | undefined) => { if (!toDA && source) { - fixSourcePaths(source); + source.path = fixPath(source.path); } }); return msg; } -function convertPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePaths: (toDA: boolean, source: DebugProtocol.Source | undefined) => void): void { - - const tmpSource: DebugProtocol.Source = { - path: '' - }; +function convertPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePath: (toDA: boolean, source: PathContainer | undefined) => void): void { switch (msg.type) { case 'event': const event = msg; switch (event.event) { case 'output': - fixSourcePaths(false, (event).body.source); + fixSourcePath(false, (event).body.source); break; case 'loadedSource': - fixSourcePaths(false, (event).body.source); + fixSourcePath(false, (event).body.source); break; case 'breakpoint': - fixSourcePaths(false, (event).body.breakpoint.source); + fixSourcePath(false, (event).body.breakpoint.source); break; default: break; @@ -160,22 +166,16 @@ function convertPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePaths: (toDA: const request = msg; switch (request.command) { case 'setBreakpoints': - fixSourcePaths(true, (request.arguments).source); + fixSourcePath(true, (request.arguments).source); break; case 'source': - fixSourcePaths(true, (request.arguments).source); + fixSourcePath(true, (request.arguments).source); break; case 'gotoTargets': - fixSourcePaths(true, (request.arguments).source); + fixSourcePath(true, (request.arguments).source); break; case 'launchVSCode': - request.arguments.args.forEach(a => { - if (a.path) { - tmpSource.path = a.path; - fixSourcePaths(false, tmpSource); - a.path = tmpSource.path; - } - }); + request.arguments.args.forEach(arg => fixSourcePath(false, arg)); break; default: break; @@ -186,24 +186,19 @@ function convertPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePaths: (toDA: if (response.success) { switch (response.command) { case 'stackTrace': - const r1 = response; - r1.body.stackFrames.forEach(frame => fixSourcePaths(false, frame.source)); + (response).body.stackFrames.forEach(frame => fixSourcePath(false, frame.source)); break; case 'loadedSources': - const r2 = response; - r2.body.sources.forEach(source => fixSourcePaths(false, source)); + (response).body.sources.forEach(source => fixSourcePath(false, source)); break; case 'scopes': - const r3 = response; - r3.body.scopes.forEach(scope => fixSourcePaths(false, scope.source)); + (response).body.scopes.forEach(scope => fixSourcePath(false, scope.source)); break; case 'setFunctionBreakpoints': - const r4 = response; - r4.body.breakpoints.forEach(bp => fixSourcePaths(false, bp.source)); + (response).body.breakpoints.forEach(bp => fixSourcePath(false, bp.source)); break; case 'setBreakpoints': - const r5 = response; - r5.body.breakpoints.forEach(bp => fixSourcePaths(false, bp.source)); + (response).body.breakpoints.forEach(bp => fixSourcePath(false, bp.source)); break; default: break; diff --git a/src/vs/workbench/parts/debug/electron-browser/callStackView.ts b/src/vs/workbench/parts/debug/electron-browser/callStackView.ts index 06797efabb7..aef0de79a27 100644 --- a/src/vs/workbench/parts/debug/electron-browser/callStackView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/callStackView.ts @@ -223,8 +223,12 @@ export class CallStackView extends ViewletPanel { })); } - layoutBody(size: number): void { - this.tree.layout(size); + layoutBody(height: number, width: number): void { + this.tree.layout(height, width); + } + + focus(): void { + this.tree.domFocus(); } private updateTreeSelection(): void { diff --git a/src/vs/workbench/parts/debug/electron-browser/debugHover.ts b/src/vs/workbench/parts/debug/electron-browser/debugHover.ts index 3cc5eabfdbe..061d9a72a1b 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugHover.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugHover.ts @@ -85,7 +85,8 @@ export class DebugHoverWidget implements IContentWidget { this.dataSource, { ariaLabel: nls.localize('treeAriaLabel', "Debug Hover"), accessibilityProvider: new DebugHoverAccessibilityProvider(), - mouseSupport: false + mouseSupport: false, + horizontalScrolling: true }, this.contextKeyService, this.listService, this.themeService, this.configurationService, this.keybindingService); this.valueContainer = $('.value'); @@ -254,7 +255,7 @@ export class DebugHoverWidget implements IContentWidget { private layoutTreeAndContainer(): void { const treeHeight = Math.min(MAX_TREE_HEIGHT, this.tree.visibleNodeCount * 18); this.treeContainer.style.height = `${treeHeight}px`; - this.tree.layout(treeHeight); + this.tree.layout(treeHeight, 324); } hide(): void { diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index 8af829fa6e7..1ec06113de4 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -45,8 +45,8 @@ import { DebugSession } from 'vs/workbench/parts/debug/electron-browser/debugSes import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_MODE, IThread, IDebugConfiguration, VIEWLET_ID, REPL_ID, IConfig, ILaunch, IViewModel, IConfigurationManager, IDebugModel, IEnablement, IBreakpoint, IBreakpointData, ICompound, IGlobalConfig, IStackFrame, AdapterEndEvent, getStateLabel } from 'vs/workbench/parts/debug/common/debug'; import { isExtensionHostDebugging } from 'vs/workbench/parts/debug/common/debugUtils'; -import { RunOnceScheduler } from 'vs/base/common/async'; import { isErrorWithActions, createErrorWithActions } from 'vs/base/common/errorsWithActions'; +import { RunOnceScheduler } from 'vs/base/common/async'; const DEBUG_BREAKPOINTS_KEY = 'debug.breakpoint'; const DEBUG_BREAKPOINTS_ACTIVATED_KEY = 'debug.breakpointactivated'; @@ -490,10 +490,9 @@ export class DebugService implements IDebugService { private registerSessionListeners(session: IDebugSession): void { const sessionRunningScheduler = new RunOnceScheduler(() => { - // Do not immediatly focus another session or thread if a session is running - // Stepping in a session should preserve that session focused even if some continued events happen + // Do not immediatly defocus the stack frame if the session is running if (session.state === State.Running && this.viewModel.focusedSession === session) { - this.focusStackFrame(undefined); + this.viewModel.setFocus(undefined, this.viewModel.focusedThread, session, false); } }, 200); this.toDispose.push(session.onDidChangeState(() => { @@ -717,7 +716,7 @@ export class DebugService implements IDebugService { // task is already running - nothing to do. return Promise.resolve(null); } - once(e => e.kind === TaskEventKind.Active && e.taskId === task._id, this.taskService.onDidStateChange)(() => { + once(e => ((e.kind === TaskEventKind.Active) || (e.kind === TaskEventKind.DependsOnStarted)) && e.taskId === task._id, this.taskService.onDidStateChange)(() => { // Task is active, so everything seems to be fine, no need to prompt after 10 seconds // Use case being a slow running task should not be prompted even though it takes more than 10 seconds taskStarted = true; diff --git a/src/vs/workbench/parts/debug/electron-browser/repl.ts b/src/vs/workbench/parts/debug/electron-browser/repl.ts index 5183a792b49..45f77ae9975 100644 --- a/src/vs/workbench/parts/debug/electron-browser/repl.ts +++ b/src/vs/workbench/parts/debug/electron-browser/repl.ts @@ -269,7 +269,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati this.replDelegate.setWidth(dimension.width - 25, this.characterWidth); const treeHeight = dimension.height - this.replInputHeight; this.treeContainer.style.height = `${treeHeight}px`; - this.tree.layout(treeHeight); + this.tree.layout(treeHeight, dimension.width); } this.replInputContainer.style.height = `${this.replInputHeight}px`; @@ -358,7 +358,8 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati accessibilityProvider: new ReplAccessibilityProvider(), identityProvider: { getId: element => element.getId() }, mouseSupport: false, - keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: e => e } + keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: e => e }, + horizontalScrolling: false }, this.contextKeyService, this.listService, this.themeService, this.configurationService, this.keybindingService); this.toDispose.push(this.tree.onContextMenu(e => this.onContextMenu(e))); diff --git a/src/vs/workbench/parts/debug/electron-browser/variablesView.ts b/src/vs/workbench/parts/debug/electron-browser/variablesView.ts index a1c4b9b5666..9540ae417cd 100644 --- a/src/vs/workbench/parts/debug/electron-browser/variablesView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/variablesView.ts @@ -117,8 +117,12 @@ export class VariablesView extends ViewletPanel { })); } - layoutBody(size: number): void { - this.tree.layout(size); + layoutBody(width: number, height: number): void { + this.tree.layout(width, height); + } + + focus(): void { + this.tree.domFocus(); } private onMouseDblClick(e: ITreeMouseEvent): void { diff --git a/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts b/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts index 409b49ad7ff..7e461ac86a3 100644 --- a/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts @@ -113,8 +113,12 @@ export class WatchExpressionsView extends ViewletPanel { })); } - layoutBody(size: number): void { - this.tree.layout(size); + layoutBody(height: number, width: number): void { + this.tree.layout(height, width); + } + + focus(): void { + this.tree.domFocus(); } private onMouseDblClick(e: ITreeMouseEvent): void { diff --git a/src/vs/workbench/parts/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/parts/extensions/browser/extensionsWidgets.ts deleted file mode 100644 index b70f618d51a..00000000000 --- a/src/vs/workbench/parts/extensions/browser/extensionsWidgets.ts +++ /dev/null @@ -1,135 +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 'vs/css!./media/extensionsWidgets'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { IExtension, IExtensionsWorkbenchService, IExtensionContainer } from '../common/extensions'; -import { append, $, addClass } from 'vs/base/browser/dom'; -import * as platform from 'vs/base/common/platform'; -import { localize } from 'vs/nls'; - -export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this.update(); } - update(): void { this.render(); } - abstract render(): void; -} - -export class Label extends ExtensionWidget { - - constructor( - private element: HTMLElement, - private fn: (extension: IExtension) => string, - @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService - ) { - super(); - this.render(); - } - - render(): void { - this.element.textContent = this.extension ? this.fn(this.extension) : ''; - } -} - -export class InstallCountWidget extends ExtensionWidget { - - constructor( - private container: HTMLElement, - private small: boolean, - @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService - ) { - super(); - addClass(container, 'extension-install-count'); - this.render(); - } - - render(): void { - this.container.innerHTML = ''; - - if (!this.extension) { - return; - } - - const installCount = this.extension.installCount; - - if (installCount === undefined) { - return; - } - - let installLabel: string; - - if (this.small) { - if (installCount > 1000000) { - installLabel = `${Math.floor(installCount / 100000) / 10}M`; - } else if (installCount > 1000) { - installLabel = `${Math.floor(installCount / 1000)}K`; - } else { - installLabel = String(installCount); - } - } - else { - installLabel = installCount.toLocaleString(platform.locale); - } - - append(this.container, $('span.octicon.octicon-cloud-download')); - const count = append(this.container, $('span.count')); - count.textContent = installLabel; - } -} - -export class RatingsWidget extends ExtensionWidget { - - constructor( - private container: HTMLElement, - private small: boolean, - @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService - ) { - super(); - addClass(container, 'extension-ratings'); - - if (this.small) { - addClass(container, 'small'); - } - - this.render(); - } - - render(): void { - this.container.innerHTML = ''; - - if (!this.extension) { - return; - } - - if (this.extension.rating === undefined) { - return; - } - - if (this.small && !this.extension.ratingCount) { - return; - } - - const rating = Math.round(this.extension.rating * 2) / 2; - - if (this.small) { - append(this.container, $('span.full.star')); - - const count = append(this.container, $('span.count')); - count.textContent = String(rating); - } else { - for (let i = 1; i <= 5; i++) { - if (rating >= i) { - append(this.container, $('span.full.star')); - } else if (rating >= i - 0.5) { - append(this.container, $('span.half.star')); - } else { - append(this.container, $('span.empty.star')); - } - } - } - this.container.title = this.extension.ratingCount > 1 ? localize('ratedByUsers', "Rated by {0} users", this.extension.ratingCount) : localize('ratedBySingleUser', "Rated by 1 user"); - } -} diff --git a/src/vs/workbench/parts/extensions/common/extensions.ts b/src/vs/workbench/parts/extensions/common/extensions.ts index 5c8339ab05a..803a1e6a512 100644 --- a/src/vs/workbench/parts/extensions/common/extensions.ts +++ b/src/vs/workbench/parts/extensions/common/extensions.ts @@ -60,7 +60,6 @@ export interface IExtension { getChangelog(token: CancellationToken): Promise; hasChangelog(): boolean; local?: ILocalExtension; - locals?: ILocalExtension[]; gallery?: IGalleryExtension; isMalicious: boolean; } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts index c5333366afe..763454e1e94 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts @@ -25,10 +25,10 @@ import { IExtensionManifest, IKeyBinding, IView, IViewContainer, ExtensionType } import { ResolvedKeybinding, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ExtensionsInput } from 'vs/workbench/parts/extensions/common/extensionsInput'; import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, IExtension, IExtensionDependencies, ExtensionContainers } from 'vs/workbench/parts/extensions/common/extensions'; -import { RatingsWidget, InstallCountWidget } from 'vs/workbench/parts/extensions/browser/extensionsWidgets'; +import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget } from 'vs/workbench/parts/extensions/electron-browser/extensionsWidgets'; import { EditorOptions } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, DisabledStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; +import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; import { WebviewElement } from 'vs/workbench/parts/webview/electron-browser/webviewElement'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -150,6 +150,7 @@ export class ExtensionEditor extends BaseEditor { static readonly ID: string = 'workbench.editor.extension'; + private iconContainer: HTMLElement; private icon: HTMLImageElement; private name: HTMLElement; private identifier: HTMLElement; @@ -207,7 +208,8 @@ export class ExtensionEditor extends BaseEditor { const root = append(parent, $('.extension-editor')); this.header = append(root, $('.header')); - this.icon = append(this.header, $('img.icon', { draggable: false })); + this.iconContainer = append(this.header, $('.icon-container')); + this.icon = append(this.iconContainer, $('img.icon', { draggable: false })); const details = append(this.header, $('.details')); const title = append(details, $('.title')); @@ -285,6 +287,7 @@ export class ExtensionEditor extends BaseEditor { this.extensionManifest = new Cache(() => createCancelablePromise(token => extension.getManifest(token))); this.extensionDependencies = new Cache(() => createCancelablePromise(token => this.extensionsWorkbenchService.loadDependencies(extension, token))); + const remoteBadge = this.instantiationService.createInstance(RemoteBadgeWidget, this.iconContainer); const onError = Event.once(domEvent(this.icon, 'error')); onError(() => this.icon.src = extension.iconUrlFallback, null, this.transientDisposables); this.icon.src = extension.iconUrl; @@ -350,6 +353,7 @@ export class ExtensionEditor extends BaseEditor { } const widgets = [ + remoteBadge, this.instantiationService.createInstance(InstallCountWidget, this.installCount, false), this.instantiationService.createInstance(RatingsWidget, this.rating, false) ]; @@ -357,18 +361,17 @@ export class ExtensionEditor extends BaseEditor { const actions = [ reloadAction, this.instantiationService.createInstance(UpdateAction), - this.instantiationService.createInstance(EnableDropDownAction, runningExtensions), + this.instantiationService.createInstance(EnableDropDownAction), this.instantiationService.createInstance(DisableDropDownAction, runningExtensions), this.instantiationService.createInstance(CombinedInstallAction), this.instantiationService.createInstance(MaliciousStatusLabelAction, true), - this.instantiationService.createInstance(DisabledStatusLabelAction, runningExtensions) ]; const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]); extensionContainers.extension = extension; this.extensionActionBar.clear(); this.extensionActionBar.push(actions, { icon: true, label: true }); - this.transientDisposables.push(...[...actions, extensionContainers]); + this.transientDisposables.push(...[...actions, ...widgets, extensionContainers]); this.setSubText(extension, reloadAction); this.content.innerHTML = ''; // Clear content before setting navbar actions. diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts index b26c648600a..013105be402 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/extensionActions'; import { localize } from 'vs/nls'; import { IAction, Action } from 'vs/base/common/actions'; -import { Throttler } from 'vs/base/common/async'; +import { ThrottledDelayer } from 'vs/base/common/async'; import * as DOM from 'vs/base/browser/dom'; import * as paths from 'vs/base/common/paths'; import { Event } from 'vs/base/common/event'; @@ -16,9 +16,9 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewlet, AutoUpdateConfigurationKey, IExtensionContainer } from 'vs/workbench/parts/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/parts/extensions/common/extensionsFileTemplate'; -import { IExtensionEnablementService, IExtensionTipsService, EnablementState, ExtensionsLabel, IExtensionRecommendation, IGalleryExtension, IExtensionsConfigContent, IExtensionManagementServerService, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionEnablementService, IExtensionTipsService, EnablementState, ExtensionsLabel, IExtensionRecommendation, IGalleryExtension, IExtensionsConfigContent, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { isUIExtension, ExtensionIdentifier, ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType } 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'; @@ -54,9 +54,6 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { clipboard } from 'electron'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { alert } from 'vs/base/browser/ui/aria/aria'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { coalesce } from 'vs/base/common/arrays'; const promptDownloadManually = (extension: IGalleryExtension | undefined, message: string, error: Error, instantiationService: IInstantiationService, notificationService: INotificationService, openerService: IOpenerService) => { @@ -515,7 +512,7 @@ export class ManageExtensionAction extends ExtensionDropDownAction { const groups: ExtensionAction[][] = []; groups.push([ this.instantiationService.createInstance(EnableGloballyAction), - this.instantiationService.createInstance(CombinedEnableForWorkspaceAction, runningExtensions) + this.instantiationService.createInstance(EnableForWorkspaceAction) ]); groups.push([ this.instantiationService.createInstance(DisableGloballyAction, runningExtensions), @@ -630,50 +627,6 @@ export class ExtensionInfoAction extends ExtensionAction { } } -export class CombinedEnableForWorkspaceAction extends ExtensionAction { - - static readonly ID = 'extensions.enableForWorkspace'; - static LABEL = localize('enableForWorkspaceAction', "Enable (Workspace)"); - - private enableForWorkspaceAction: EnableForWorkspaceAction; - private installInRemoteServerAction: InstallInRemoteServerAction; - - constructor(readonly runningExtensions: IExtensionDescription[], - @IInstantiationService instantiationService: IInstantiationService - ) { - super(CombinedEnableForWorkspaceAction.ID, CombinedEnableForWorkspaceAction.LABEL); - - this.enableForWorkspaceAction = instantiationService.createInstance(EnableForWorkspaceAction); - this.installInRemoteServerAction = instantiationService.createInstance(InstallInRemoteServerAction, runningExtensions); - - this.update(); - } - - update(): void { - this.enableForWorkspaceAction.extension = this.extension; - this.installInRemoteServerAction.extension = this.extension; - this.enableForWorkspaceAction.update(); - this.installInRemoteServerAction.update(); - this.enabled = this.installInRemoteServerAction.enabled || this.enableForWorkspaceAction.enabled; - } - - run(): Promise { - if (this.installInRemoteServerAction.enabled) { - return this.installInRemoteServerAction.run(); - } - if (this.enableForWorkspaceAction.enabled) { - return this.enableForWorkspaceAction.run(); - } - return Promise.resolve(); - } - - dispose(): void { - super.dispose(); - this.enableForWorkspaceAction.dispose(); - this.installInRemoteServerAction.dispose(); - } -} - export class EnableForWorkspaceAction extends ExtensionAction { static readonly ID = 'extensions.enableForWorkspace'; @@ -699,65 +652,6 @@ export class EnableForWorkspaceAction extends ExtensionAction { } } -export class InstallInRemoteServerAction extends ExtensionAction { - - static readonly ID = 'extensions.installInRemoteServerAction'; - static LABEL = localize('enableForWorkspaceAction', "Enable (Workspace)"); - - constructor(readonly runningExtensions: IExtensionDescription[], - @IExtensionsWorkbenchService extensionWorkbenchService: IExtensionsWorkbenchService, - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IStorageService private readonly storageService: IStorageService, - @ILabelService private readonly labelService: ILabelService, - @IDialogService private readonly dialogService: IDialogService, - @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, - @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService - ) { - super(InstallInRemoteServerAction.ID, InstallInRemoteServerAction.LABEL); - this.update(); - } - - update(): void { - this.enabled = false; - if (this.extensionManagementServerService.remoteExtensionManagementServer - && this.extension && this.extension.locals && this.extension.locals.length > 0 - && !isUIExtension(this.extension.locals[0].manifest, this.configurationService) - && this.extension.state === ExtensionState.Installed) { - const installedInRemoteServer = this.extension.locals.some(local => { - const server = this.extensionManagementServerService.getExtensionManagementServer(local.location); - return !!server && server.authority === this.extensionManagementServerService.remoteExtensionManagementServer!.authority; - }); - if (!installedInRemoteServer) { - this.enabled = !this.runningExtensions.some(e => areSameExtensions({ id: e.identifier.value }, this.extension.identifier)); - } - } - } - - async run(): Promise { - if (!this.enabled) { - return Promise.resolve(); - } - if (this.storageService.getBoolean('askToInstallRemoteServerExtension', StorageScope.GLOBAL, true)) { - const message = localize('install extension', "Enabling the '{0}' extension will also install it in {1}. Would you like to continue?", this.extension.displayName, this.labelService.getHostLabel() || this.extensionManagementServerService.remoteExtensionManagementServer!.authority); - const response = await this.dialogService.confirm({ type: 'info', message, checkbox: { label: localize('do not ask me again', "Do not ask me again") } }); - if (!response || !response.confirmed) { - return Promise.resolve(); - } - if (response.checkboxChecked) { - this.storageService.store('askToInstallRemoteServerExtension', false, StorageScope.GLOBAL); - } - } - const galleryExtension = this.extension.gallery ? this.extension.gallery : await this.extensionGalleryService.getExtension(this.extension.local!.identifier); - if (galleryExtension) { - return this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.installFromGallery(galleryExtension); - } else { - const zipLocation = await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.zip(this.extension.local!); - return this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.unzip(zipLocation, this.extension.type!); - } - } -} - export class EnableGloballyAction extends ExtensionAction { static readonly ID = 'extensions.enableGlobally'; @@ -765,9 +659,7 @@ export class EnableGloballyAction extends ExtensionAction { constructor( @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService, - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService ) { super(EnableGloballyAction.ID, EnableGloballyAction.LABEL); this.update(); @@ -775,15 +667,7 @@ export class EnableGloballyAction extends ExtensionAction { update(): void { this.enabled = false; - if (this.extension && this.extension.locals && this.extension.local) { - if (!isUIExtension(this.extension.local.manifest, this.configurationService) && this.extensionManagementServerService.remoteExtensionManagementServer) { - if (!this.extension.locals.some(local => { - const server = this.extensionManagementServerService.getExtensionManagementServer(local.location); - return !!server && server.authority === this.extensionManagementServerService.remoteExtensionManagementServer!.authority; - })) { - return; - } - } + if (this.extension && this.extension.local) { this.enabled = this.extension.state === ExtensionState.Installed && this.extension.enablementState === EnablementState.Disabled && this.extensionEnablementService.canChangeEnablement(this.extension.local); } } @@ -891,12 +775,11 @@ export abstract class ExtensionEditorDropDownAction extends ExtensionDropDownAct export class EnableDropDownAction extends ExtensionEditorDropDownAction { constructor( - runningExtensions: IExtensionDescription[], @IInstantiationService instantiationService: IInstantiationService ) { super('extensions.enable', localize('enableAction', "Enable"), [ instantiationService.createInstance(EnableGloballyAction), - instantiationService.createInstance(CombinedEnableForWorkspaceAction, runningExtensions) + instantiationService.createInstance(EnableForWorkspaceAction) ], instantiationService); } } @@ -1073,25 +956,24 @@ export class ReloadAction extends ExtensionAction { private static readonly EnabledClass = 'extension-action reload'; private static readonly DisabledClass = `${ReloadAction.EnabledClass} disabled`; - private throttler: Throttler; + // Use delayer to wait for more updates + private throttler: ThrottledDelayer; private disposables: IDisposable[] = []; constructor( @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IWindowService private readonly windowService: IWindowService, @IExtensionService private readonly extensionService: IExtensionService, - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService ) { super('extensions.reload', localize('reloadAction', "Reload"), ReloadAction.DisabledClass, false); - this.throttler = new Throttler(); + this.throttler = new ThrottledDelayer(50); this.extensionService.onDidChangeExtensions(this.update, this, this.disposables); this.update(); } - update(): void { - this.throttler.queue(() => { + update(): Promise { + return this.throttler.trigger(() => { this.enabled = false; this.tooltip = ''; if (!this.extension) { @@ -1136,33 +1018,14 @@ export class ReloadAction extends ExtensionAction { return; } } else { - const uiExtension = isUIExtension(installed.local.manifest, this.configurationService); if (!isDisabled) { - let enableReload = true; - if (this.extensionManagementServerService.remoteExtensionManagementServer && installed.locals) { - if (uiExtension) { - // Only UI extension from local server requires reload if it is not running on the server - enableReload = installed.locals.some(local => { - const server = this.extensionManagementServerService.getExtensionManagementServer(local.location); - return !!server && server.authority === this.extensionManagementServerService.localExtensionManagementServer.authority; - }); - } else { - enableReload = installed.locals.some(local => { - const server = this.extensionManagementServerService.getExtensionManagementServer(local.location); - return !!server && server.authority === this.extensionManagementServerService.remoteExtensionManagementServer!.authority; - }); - } - } - - if (enableReload === true) { - this.enabled = true; - if (!isEnabled) { - this.tooltip = localize('postInstallTooltip', "Please reload Visual Studio Code to complete the installation of this extension."); - } else { - this.tooltip = localize('postEnableTooltip', "Please reload Visual Studio Code to complete the enabling of this extension."); - } - return; + this.enabled = true; + if (!isEnabled) { + this.tooltip = localize('postInstallTooltip', "Please reload Visual Studio Code to complete the installation of this extension."); + } else { + this.tooltip = localize('postEnableTooltip', "Please reload Visual Studio Code to complete the enabling of this extension."); } + return; } } return; @@ -1661,44 +1524,6 @@ export class ChangeSortAction extends Action { } } -export class ChangeGroupAction extends Action { - - private query: Query; - private disposables: IDisposable[] = []; - - constructor( - id: string, - label: string, - onSearchChange: Event, - private groupBy: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - - if (groupBy === undefined) { - throw new Error('bad arguments'); - } - - this.query = Query.parse(''); - onSearchChange(this.onSearchChange, this, this.disposables); - this.onSearchChange(''); - } - - private onSearchChange(value: string): void { - const query = Query.parse(value); - this.query = new Query(query.value, query.sortBy, this.groupBy || query.groupBy); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search(this.query.toString()); - viewlet.focus(); - }); - } -} - export class ConfigureRecommendedExtensionsCommandsContributor extends Disposable implements IWorkbenchContribution { private workspaceContextKey = new RawContextKey('workspaceRecommendations', true); @@ -2235,43 +2060,6 @@ export class MaliciousStatusLabelAction extends ExtensionAction { } } -export class DisabledStatusLabelAction extends ExtensionAction { - - private static readonly Class = 'disable-status'; - - constructor( - private runningExtensions: IExtensionDescription[], - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @ILabelService private readonly labelService: ILabelService - ) { - super('extensions.install', localize('disabled', "Disabled"), `${DisabledStatusLabelAction.Class} hide`, false); - this.update(); - } - - update(): void { - this.class = `${DisabledStatusLabelAction.Class} hide`; - this.tooltip = ''; - if (this.extension && this.extension.local && !this.extension.isMalicious && !this.runningExtensions.some(e => ExtensionIdentifier.equals(e.identifier, this.extension.identifier.id))) { - if (this.extensionManagementServerService.remoteExtensionManagementServer && !isUIExtension(this.extension.local.manifest, this.configurationService) && this.extension.locals) { - const installedInRemoteServer = this.extension.locals.some(local => { - const server = this.extensionManagementServerService.getExtensionManagementServer(local.location); - return !!server && server.authority === this.extensionManagementServerService.remoteExtensionManagementServer!.authority; - }); - if (!installedInRemoteServer) { - this.class = `${DisabledStatusLabelAction.Class}`; - this.label = localize('disabled NonUI Extension', "Disabled for this Workspace because it is not installed in {0}.", this.labelService.getHostLabel() || this.extensionManagementServerService.remoteExtensionManagementServer.authority); - return; - } - } - } - } - - run(): Promise { - return Promise.resolve(null); - } -} - export class DisableAllAction extends Action { static readonly ID = 'workbench.extensions.action.disableAll'; diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts index e2323ca0895..fb69a556f6d 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { append, $, addClass, removeClass, toggleClass } from 'vs/base/browser/dom'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Action } from 'vs/base/common/actions'; @@ -14,12 +13,11 @@ import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { Event } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { IExtension, IExtensionsWorkbenchService, ExtensionContainers } from 'vs/workbench/parts/extensions/common/extensions'; -import { InstallAction, UpdateAction, ManageExtensionAction, ReloadAction, extensionButtonProminentBackground, extensionButtonProminentForeground, MaliciousStatusLabelAction, ExtensionActionItem } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; +import { InstallAction, UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionItem } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { Label, RatingsWidget, InstallCountWidget } from 'vs/workbench/parts/extensions/browser/extensionsWidgets'; +import { Label, RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget } from 'vs/workbench/parts/extensions/electron-browser/extensionsWidgets'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionTipsService, IExtensionManagementServerService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IExtensionManagementServerService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { INotificationService } from 'vs/platform/notification/common/notification'; export interface IExtensionsViewState { @@ -57,27 +55,17 @@ export class Renderer implements IPagedRenderer { @INotificationService private readonly notificationService: INotificationService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionService private readonly extensionService: IExtensionService, - @IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService, - @IThemeService private readonly themeService: IThemeService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService ) { } get templateId() { return 'extension'; } renderTemplate(root: HTMLElement): ITemplateData { - const bookmark = append(root, $('span.bookmark')); - append(bookmark, $('span.octicon.octicon-star')); - const applyBookmarkStyle = (theme) => { - const bgColor = theme.getColor(extensionButtonProminentBackground); - const fgColor = theme.getColor(extensionButtonProminentForeground); - bookmark.style.borderTopColor = bgColor ? bgColor.toString() : 'transparent'; - bookmark.style.color = fgColor ? fgColor.toString() : 'white'; - }; - applyBookmarkStyle(this.themeService.getTheme()); - const bookmarkStyler = this.themeService.onThemeChange(applyBookmarkStyle.bind(this)); - + const recommendationWidget = this.instantiationService.createInstance(RecommendationWidget, root); const element = append(root, $('.extension')); - const icon = append(element, $('img.icon')); + const iconContainer = append(element, $('.icon-container')); + const icon = append(iconContainer, $('img.icon')); + const badgeWidget = this.instantiationService.createInstance(RemoteBadgeWidget, iconContainer); const details = append(element, $('.details')); const headerContainer = append(details, $('.header-container')); const header = append(headerContainer, $('.header')); @@ -100,6 +88,8 @@ export class Renderer implements IPagedRenderer { actionbar.onDidRun(({ error }) => error && this.notificationService.error(error)); const widgets = [ + recommendationWidget, + badgeWidget, this.instantiationService.createInstance(Label, version, (e: IExtension) => e.version), this.instantiationService.createInstance(InstallCountWidget, installCount, true), this.instantiationService.createInstance(RatingsWidget, ratings, true) @@ -114,7 +104,7 @@ export class Renderer implements IPagedRenderer { const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]); actionbar.push(actions, actionOptions); - const disposables = [...actions, ...widgets, actionbar, bookmarkStyler, extensionContainers]; + const disposables = [...actions, ...widgets, actionbar, extensionContainers]; return { root, element, icon, name, installCount, ratings, author, description, disposables, actionbar, @@ -178,13 +168,6 @@ export class Renderer implements IPagedRenderer { data.icon.style.visibility = 'inherit'; } - this.updateRecommendationStatus(extension, data); - data.extensionDisposables.push(this.extensionTipsService.onRecommendationChange(change => { - if (areSameExtensions({ id: change.extensionId }, extension.identifier)) { - this.updateRecommendationStatus(extension, data); - } - })); - data.name.textContent = extension.displayName; data.author.textContent = extension.publisherDisplayName; data.description.textContent = extension.description; @@ -209,24 +192,6 @@ export class Renderer implements IPagedRenderer { }, this, data.extensionDisposables); } - private updateRecommendationStatus(extension: IExtension, data: ITemplateData) { - const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); - let ariaLabel = extension.displayName + '. '; - - if (!extRecommendations[extension.identifier.id.toLowerCase()]) { - removeClass(data.root, 'recommended'); - data.root.title = ''; - } else { - addClass(data.root, 'recommended'); - ariaLabel += extRecommendations[extension.identifier.id.toLowerCase()].reasonText + ' '; - data.root.title = extRecommendations[extension.identifier.id.toLowerCase()].reasonText; - } - - ariaLabel += localize('viewExtensionDetailsAria', "Press enter for extension details."); - data.root.setAttribute('aria-label', ariaLabel); - - } - disposeTemplate(data: ITemplateData): void { data.disposables = dispose(data.disposables); } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts index b90011dc02d..352820e161a 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts @@ -22,7 +22,7 @@ import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, ExtensionS import { ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowDisabledExtensionsAction, ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, - EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction, ChangeGroupAction + EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; import { IExtensionManagementService, IExtensionManagementServerService, IExtensionManagementServer, EnablementState } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionsInput } from 'vs/workbench/parts/extensions/common/extensionsInput'; @@ -35,7 +35,7 @@ import { IActivityService, ProgressBadge, NumberBadge } from 'vs/workbench/servi import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ViewsRegistry, IViewDescriptor } from 'vs/workbench/common/views'; -import { ViewContainerViewlet, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { ViewContainerViewlet } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -47,9 +47,6 @@ import { IWindowService } from 'vs/platform/windows/common/windows'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IAddedViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; import { ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { ExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/node/extensionsWorkbenchService'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Query } from 'vs/workbench/parts/extensions/common/extensionQuery'; import { SuggestEnabledInput, attachSuggestEnabledInputBoxStyler } from 'vs/workbench/parts/codeEditor/electron-browser/suggestEnabledInput'; import { alert } from 'vs/base/browser/ui/aria/aria'; @@ -279,7 +276,6 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio private extensionsBox: HTMLElement; private primaryActions: IAction[]; private secondaryActions: IAction[]; - private groupByServerAction: IAction; private disposables: IDisposable[] = []; constructor( @@ -297,8 +293,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio @IWorkspaceContextService contextService: IWorkspaceContextService, @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, - @IExtensionService extensionService: IExtensionService, - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService + @IExtensionService extensionService: IExtensionService ) { super(VIEWLET_ID, `${VIEWLET_ID}.state`, true, configurationService, partService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); @@ -395,12 +390,6 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio getSecondaryActions(): IAction[] { if (!this.secondaryActions) { - if (!this.groupByServerAction) { - this.groupByServerAction = this.instantiationService.createInstance(ChangeGroupAction, 'extensions.group.servers', localize('group by servers', "Group By: Server"), this.onSearchChange, 'server'); - this.disposables.push(this.onSearchChange(value => { - this.groupByServerAction.enabled = !value || ExtensionsListView.isInstalledExtensionsQuery(value) || ExtensionsListView.isBuiltInExtensionsQuery(value); - })); - } this.secondaryActions = [ this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL), this.instantiationService.createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, ShowOutdatedExtensionsAction.LABEL), @@ -414,7 +403,6 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.rating', localize('sort by rating', "Sort By: Rating"), this.onSearchChange, 'rating'), this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.name', localize('sort by name', "Sort By: Name"), this.onSearchChange, 'name'), new Separator(), - ...(this.extensionManagementServerService.remoteExtensionManagementServer ? [this.groupByServerAction, new Separator()] : []), this.instantiationService.createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL), ...(this.configurationService.getValue(AutoUpdateConfigurationKey) ? [this.instantiationService.createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL)] : [this.instantiationService.createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL), this.instantiationService.createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL)]), this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL), @@ -489,21 +477,6 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio } } - protected createView(viewDescriptor: IViewDescriptor, options: IViewletViewOptions): ViewletPanel { - if (this.extensionManagementServerService.remoteExtensionManagementServer) { - const extensionManagementServer = viewDescriptor.id === `server.extensionsList.${this.extensionManagementServerService.localExtensionManagementServer.authority}` ? this.extensionManagementServerService.localExtensionManagementServer - : viewDescriptor.id === `server.extensionsList.${this.extensionManagementServerService.remoteExtensionManagementServer.authority}` ? this.extensionManagementServerService.remoteExtensionManagementServer : null; - if (extensionManagementServer) { - const servicesCollection: ServiceCollection = new ServiceCollection(); - servicesCollection.set(IExtensionManagementService, extensionManagementServer.extensionManagementService); - servicesCollection.set(IExtensionsWorkbenchService, new SyncDescriptor(ExtensionsWorkbenchService)); - const instantiationService = this.instantiationService.createChild(servicesCollection); - return instantiationService.createInstance(viewDescriptor.ctor, options, [extensionManagementServer]) as ViewletPanel; - } - } - return this.instantiationService.createInstance(viewDescriptor.ctor, options) as ViewletPanel; - } - private count(): number { return this.panels.reduce((count, view) => (view).count() + count, 0); } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts index d28726e3bb0..58e70c850e1 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts @@ -110,7 +110,8 @@ export class ExtensionsListView extends ViewletPanel { this.list = this.instantiationService.createInstance(WorkbenchPagedList, this.extensionsList, delegate, [renderer], { ariaLabel: localize('extensions', "Extensions"), multipleSelectionSupport: false, - setRowLineHeight: false + setRowLineHeight: false, + horizontalScrolling: false }) as WorkbenchPagedList; this.list.onContextMenu(e => this.onContextMenu(e), this, this.disposables); this.list.onFocusChange(e => extensionsViewState.onFocusChange(e.elements), this, this.disposables); @@ -128,9 +129,9 @@ export class ExtensionsListView extends ViewletPanel { .on(this.pin, this, this.disposables); } - layoutBody(size: number): void { - this.extensionsList.style.height = size + 'px'; - this.list.layout(size); + protected layoutBody(height: number, width: number): void { + this.extensionsList.style.height = height + 'px'; + this.list.layout(height, width); } async show(query: string): Promise> { diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts new file mode 100644 index 00000000000..6a49044725b --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts @@ -0,0 +1,249 @@ +/*--------------------------------------------------------------------------------------------- + * 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/extensionsWidgets'; +import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { IExtension, IExtensionsWorkbenchService, IExtensionContainer } from '../common/extensions'; +import { append, $, addClass } from 'vs/base/browser/dom'; +import * as platform from 'vs/base/common/platform'; +import { localize } from 'vs/nls'; +import { IExtensionManagementServerService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { extensionButtonProminentBackground, extensionButtonProminentForeground } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_FOREGROUND } from 'vs/workbench/common/theme'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; + +export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + update(): void { this.render(); } + abstract render(): void; +} + +export class Label extends ExtensionWidget { + + constructor( + private element: HTMLElement, + private fn: (extension: IExtension) => string, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService + ) { + super(); + this.render(); + } + + render(): void { + this.element.textContent = this.extension ? this.fn(this.extension) : ''; + } +} + +export class InstallCountWidget extends ExtensionWidget { + + constructor( + private container: HTMLElement, + private small: boolean, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService + ) { + super(); + addClass(container, 'extension-install-count'); + this.render(); + } + + render(): void { + this.container.innerHTML = ''; + + if (!this.extension) { + return; + } + + const installCount = this.extension.installCount; + + if (installCount === undefined) { + return; + } + + let installLabel: string; + + if (this.small) { + if (installCount > 1000000) { + installLabel = `${Math.floor(installCount / 100000) / 10}M`; + } else if (installCount > 1000) { + installLabel = `${Math.floor(installCount / 1000)}K`; + } else { + installLabel = String(installCount); + } + } + else { + installLabel = installCount.toLocaleString(platform.locale); + } + + append(this.container, $('span.octicon.octicon-cloud-download')); + const count = append(this.container, $('span.count')); + count.textContent = installLabel; + } +} + +export class RatingsWidget extends ExtensionWidget { + + constructor( + private container: HTMLElement, + private small: boolean + ) { + super(); + addClass(container, 'extension-ratings'); + + if (this.small) { + addClass(container, 'small'); + } + + this.render(); + } + + render(): void { + this.container.innerHTML = ''; + + if (!this.extension) { + return; + } + + if (this.extension.rating === undefined) { + return; + } + + if (this.small && !this.extension.ratingCount) { + return; + } + + const rating = Math.round(this.extension.rating * 2) / 2; + + if (this.small) { + append(this.container, $('span.full.star')); + + const count = append(this.container, $('span.count')); + count.textContent = String(rating); + } else { + for (let i = 1; i <= 5; i++) { + if (rating >= i) { + append(this.container, $('span.full.star')); + } else if (rating >= i - 0.5) { + append(this.container, $('span.half.star')); + } else { + append(this.container, $('span.empty.star')); + } + } + } + this.container.title = this.extension.ratingCount > 1 ? localize('ratedByUsers', "Rated by {0} users", this.extension.ratingCount) : localize('ratedBySingleUser', "Rated by 1 user"); + } +} + +export class RecommendationWidget extends ExtensionWidget { + + private element: HTMLElement; + private disposables: IDisposable[] = []; + + constructor( + private parent: HTMLElement, + @IThemeService private readonly themeService: IThemeService, + @IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService + ) { + super(); + this.render(); + this._register(toDisposable(() => this.clear())); + } + + private clear(): void { + this.parent.title = ''; + this.parent.setAttribute('aria-label', this.extension ? localize('viewExtensionDetailsAria', "{0}. Press enter for extension details.", this.extension.displayName) : ''); + if (this.element) { + this.parent.removeChild(this.element); + } + this.element = null; + this.disposables = dispose(this.disposables); + } + + render(): void { + this.clear(); + if (!this.extension) { + return; + } + const updateRecommendationMarker = () => { + this.clear(); + const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); + if (extRecommendations[this.extension.identifier.id.toLowerCase()]) { + this.element = append(this.parent, $('div.bookmark')); + const recommendation = append(this.element, $('.recommendation')); + append(recommendation, $('span.octicon.octicon-star')); + const applyBookmarkStyle = (theme) => { + const bgColor = theme.getColor(extensionButtonProminentBackground); + const fgColor = theme.getColor(extensionButtonProminentForeground); + recommendation.style.borderTopColor = bgColor ? bgColor.toString() : 'transparent'; + recommendation.style.color = fgColor ? fgColor.toString() : 'white'; + }; + applyBookmarkStyle(this.themeService.getTheme()); + this.themeService.onThemeChange(applyBookmarkStyle, this, this.disposables); + this.parent.title = extRecommendations[this.extension.identifier.id.toLowerCase()].reasonText; + this.parent.setAttribute('aria-label', localize('viewRecommendedExtensionDetailsAria', "{0}. {1} Press enter for extension details.", this.extension.displayName, extRecommendations[this.extension.identifier.id.toLowerCase()].reasonText)); + } + }; + updateRecommendationMarker(); + this.extensionTipsService.onRecommendationChange(() => updateRecommendationMarker(), this, this.disposables); + } + +} + + +export class RemoteBadgeWidget extends ExtensionWidget { + + private element: HTMLElement | null; + private disposables: IDisposable[] = []; + + constructor( + private parent: HTMLElement, + @ILabelService private readonly labelService: ILabelService, + @IThemeService private readonly themeService: IThemeService, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + ) { + super(); + this.render(); + this._register(toDisposable(() => this.clear())); + } + + private clear(): void { + if (this.element) { + this.parent.removeChild(this.element); + } + this.element = null; + this.disposables = dispose(this.disposables); + } + + render(): void { + this.clear(); + if (!this.extension || !this.extension.local) { + return; + } + const server = this.extensionManagementServerService.getExtensionManagementServer(this.extension.local.location); + if (server === this.extensionManagementServerService.remoteExtensionManagementServer) { + this.element = append(this.parent, $('div.extension-remote-badge')); + append(this.element, $('span.octicon.octicon-file-symlink-directory')); + + const applyBadgeStyle = () => { + const bgColor = this.themeService.getTheme().getColor(STATUS_BAR_HOST_NAME_BACKGROUND); + const fgColor = this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY ? this.themeService.getTheme().getColor(STATUS_BAR_NO_FOLDER_FOREGROUND) : this.themeService.getTheme().getColor(STATUS_BAR_FOREGROUND); + this.element.style.backgroundColor = bgColor ? bgColor.toString() : ''; + this.element.style.color = fgColor ? fgColor.toString() : ''; + }; + applyBadgeStyle(); + this.themeService.onThemeChange(applyBadgeStyle, this, this.disposables); + this.workspaceContextService.onDidChangeWorkbenchState(applyBadgeStyle, this, this.disposables); + + const updateTitle = () => this.element.title = this.labelService.getHostLabel(); + this.labelService.onDidChangeFormatters(() => updateTitle(), this, this.disposables); + updateTitle(); + } + } + +} diff --git a/src/vs/workbench/parts/extensions/browser/media/EmptyStar.svg b/src/vs/workbench/parts/extensions/electron-browser/media/EmptyStar.svg similarity index 100% rename from src/vs/workbench/parts/extensions/browser/media/EmptyStar.svg rename to src/vs/workbench/parts/extensions/electron-browser/media/EmptyStar.svg diff --git a/src/vs/workbench/parts/extensions/browser/media/FullStarLight.svg b/src/vs/workbench/parts/extensions/electron-browser/media/FullStarLight.svg similarity index 100% rename from src/vs/workbench/parts/extensions/browser/media/FullStarLight.svg rename to src/vs/workbench/parts/extensions/electron-browser/media/FullStarLight.svg diff --git a/src/vs/workbench/parts/extensions/browser/media/HalfStarLight.svg b/src/vs/workbench/parts/extensions/electron-browser/media/HalfStarLight.svg similarity index 100% rename from src/vs/workbench/parts/extensions/browser/media/HalfStarLight.svg rename to src/vs/workbench/parts/extensions/electron-browser/media/HalfStarLight.svg diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css index 9041a268d23..a5446fb5d46 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css @@ -22,12 +22,22 @@ font-size: 14px; } -.extension-editor > .header > .icon { +.extension-editor > .header > .icon-container { + position: relative; +} + +.extension-editor > .header > .icon-container .icon { height: 128px; width: 128px; object-fit: contain; } +.extension-editor > .header > .icon-container .extension-remote-badge { + position: absolute; + right: 0px; + top: 94px; +} + .extension-editor > .header > .details { padding-left: 20px; overflow: hidden; @@ -121,7 +131,7 @@ .extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .action-label { font-weight: 600; - margin: 4px; + margin: 4px 8px 4px 0px; padding: 1px 6px; } diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css index 52362be55a0..bdd6185f3dd 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench > .activitybar > .content .monaco-action-bar .action-label.extensions { +.monaco-workbench .activitybar > .content .monaco-action-bar .action-label.extensions { -webkit-mask: url('extensions-dark.svg') no-repeat 50% 50%; } diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css index e8e11338689..648b9c762a8 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css @@ -57,19 +57,18 @@ } .extensions-viewlet > .extensions .monaco-list-row > .bookmark { - display: none; -} - -.extensions-viewlet > .extensions .monaco-list-row.recommended > .bookmark { display: inline-block; height: 20px; width: 20px; +} + +.extensions-viewlet > .extensions .monaco-list-row > .bookmark > .recommendation { border-right: 20px solid transparent; border-top: 20px solid; box-sizing: border-box; } -.extensions-viewlet > .extensions .monaco-list-row > .bookmark > .octicon { +.extensions-viewlet > .extensions .monaco-list-row > .bookmark > .recommendation > .octicon { position: absolute; top: 1px; left: 1px; @@ -91,7 +90,11 @@ background: url('loading.svg') center center no-repeat; } -.extensions-viewlet > .extensions .extension > .icon { +.extensions-viewlet > .extensions .monaco-list-row > .extension > .icon-container { + position: relative; +} + +.extensions-viewlet > .extensions .extension > .icon-container > .icon { width: 42px; height: 42px; padding: 10px 14px 10px 0; @@ -99,6 +102,12 @@ object-fit: contain; } +.extensions-viewlet > .extensions .monaco-list-row > .extension > .icon-container > .extension-remote-badge { + position: absolute; + right: 5px; + bottom: 5px; +} + .extensions-viewlet.narrow > .extensions .extension > .icon { display: none; } @@ -204,8 +213,8 @@ .vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .bookmark, .vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .bookmark, -.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .icon, -.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .icon, +.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .icon-container > .icon, +.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .icon-container > .icon, .vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .details > .header-container, .vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .details > .header-container, .vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .details > .description, diff --git a/src/vs/workbench/parts/extensions/browser/media/extensionsWidgets.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensionsWidgets.css similarity index 89% rename from src/vs/workbench/parts/extensions/browser/media/extensionsWidgets.css rename to src/vs/workbench/parts/extensions/electron-browser/media/extensionsWidgets.css index 7ddd1d4c880..7146eb87f31 100644 --- a/src/vs/workbench/parts/extensions/browser/media/extensionsWidgets.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionsWidgets.css @@ -46,4 +46,12 @@ .extension-ratings.small > .count { margin-left: 2px; +} + +.extension-remote-badge { + width: 22px; + height: 22px; + line-height: 22px; + border-radius: 20px; + text-align: center; } \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts index 4642d292181..d8b3b30fe8b 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -398,7 +398,8 @@ export class RuntimeExtensionsEditor extends BaseEditor { this._list = this._instantiationService.createInstance(WorkbenchList, parent, delegate, [renderer], { multipleSelectionSupport: false, - setRowLineHeight: false + setRowLineHeight: false, + horizontalScrolling: false }) as WorkbenchList; this._list.splice(0, this._list.length, this._elements); diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index d3b7b2723d3..167b592c380 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -30,14 +30,12 @@ import product from 'vs/platform/node/product'; import { ILogService } from 'vs/platform/log/common/log'; import { IProgressService2, ProgressLocation } from 'vs/platform/progress/common/progress'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { groupBy } from 'vs/base/common/collections'; -import { Schemas } from 'vs/base/common/network'; import * as resources from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IFileService } from 'vs/platform/files/common/files'; import { IExtensionManifest, ExtensionType, ExtensionIdentifierWithVersion, IExtension as IPlatformExtension } from 'vs/platform/extensions/common/extensions'; +import { isUIExtension } from 'vs/platform/extensions/node/extensionsUtil'; interface IExtensionStateProvider { (extension: Extension): T; @@ -45,13 +43,12 @@ interface IExtensionStateProvider { class Extension implements IExtension { - public get local(): ILocalExtension { return this.locals[0]; } public enablementState: EnablementState = EnablementState.Enabled; constructor( private galleryService: IExtensionGalleryService, private stateProvider: IExtensionStateProvider, - public locals: ILocalExtension[], + public local: ILocalExtension | undefined, public gallery: IGalleryExtension | undefined, private telemetryService: ITelemetryService, private logService: ILogService, @@ -63,7 +60,7 @@ class Extension implements IExtension { } get name(): string { - return this.gallery ? this.gallery.name : this.local.manifest.name; + return this.gallery ? this.gallery.name : this.local!.manifest.name; } get displayName(): string { @@ -71,22 +68,22 @@ class Extension implements IExtension { return this.gallery.displayName || this.gallery.name; } - return this.local.manifest.displayName || this.local.manifest.name; + return this.local!.manifest.displayName || this.local!.manifest.name; } get identifier(): IExtensionIdentifier { if (this.gallery) { return this.gallery.identifier; } - return this.local.identifier; + return this.local!.identifier; } get uuid(): string | undefined { - return this.gallery ? this.gallery.identifier.uuid : this.local.identifier.uuid; + return this.gallery ? this.gallery.identifier.uuid : this.local!.identifier.uuid; } get publisher(): string { - return this.gallery ? this.gallery.publisher : this.local.manifest.publisher; + return this.gallery ? this.gallery.publisher : this.local!.manifest.publisher; } get publisherDisplayName(): string { @@ -94,11 +91,11 @@ class Extension implements IExtension { return this.gallery.publisherDisplayName || this.gallery.publisher; } - if (this.local.metadata && this.local.metadata.publisherDisplayName) { - return this.local.metadata.publisherDisplayName; + if (this.local!.metadata && this.local!.metadata.publisherDisplayName) { + return this.local!.metadata.publisherDisplayName; } - return this.local.manifest.publisher; + return this.local!.manifest.publisher; } get version(): string { @@ -106,11 +103,11 @@ class Extension implements IExtension { } get latestVersion(): string { - return this.gallery ? this.gallery.version : this.local.manifest.version; + return this.gallery ? this.gallery.version : this.local!.manifest.version; } get description(): string { - return this.gallery ? this.gallery.description : this.local.manifest.description || ''; + return this.gallery ? this.gallery.description : this.local!.manifest.description || ''; } get url(): string | undefined { @@ -145,7 +142,7 @@ class Extension implements IExtension { } private get defaultIconUrl(): string { - if (this.type === ExtensionType.System) { + if (this.type === ExtensionType.System && this.local) { if (this.local.manifest && this.local.manifest.contributes) { if (Array.isArray(this.local.manifest.contributes.themes) && this.local.manifest.contributes.themes.length) { return require.toUrl('../electron-browser/media/theme-icon.png'); @@ -194,7 +191,7 @@ class Extension implements IExtension { if (gallery) { return getGalleryExtensionTelemetryData(gallery); } else { - return getLocalExtensionTelemetryData(local); + return getLocalExtensionTelemetryData(local!); } } @@ -215,7 +212,7 @@ class Extension implements IExtension { return Promise.resolve(null); } - return Promise.resolve(this.local.manifest); + return Promise.resolve(this.local!.manifest); } hasReadme(): boolean { @@ -383,7 +380,6 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, @IWindowService private readonly windowService: IWindowService, @ILogService private readonly logService: ILogService, @IProgressService2 private readonly progressService: IProgressService2, - @IExtensionService private readonly runtimeExtensionService: IExtensionService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IStorageService private readonly storageService: IStorageService, @IFileService private readonly fileService: IFileService @@ -430,23 +426,20 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, queryLocal(): Promise { return this.extensionService.getInstalled() - .then(installed => this.getDistinctInstalledExtensions(installed) - .then(distinctInstalled => { - const installedById = index(this.installed, e => e.identifier.id); - const groupById = groupBy(installed, i => i.identifier.id); - this.installed = distinctInstalled.map(local => { - const locals = groupById[local.identifier.id]; - locals.splice(locals.indexOf(local), 1); - locals.splice(0, 0, local); - const extension = installedById[local.identifier.id] || new Extension(this.galleryService, this.stateProvider, locals, undefined, this.telemetryService, this.logService, this.fileService); - extension.locals = locals; - extension.enablementState = this.extensionEnablementService.getEnablementState(local); - return extension; - }); + .then(installed => { + if (this.extensionManagementServerService.remoteExtensionManagementServer) { + installed = installed.filter(installed => this.belongsToWindow(installed)); + } + const installedById = index(this.installed, e => e.identifier.id); + this.installed = installed.map(local => { + const extension = installedById[local.identifier.id] || new Extension(this.galleryService, this.stateProvider, local, undefined, this.telemetryService, this.logService, this.fileService); + extension.enablementState = this.extensionEnablementService.getEnablementState(local); + return extension; + }); - this._onChange.fire(undefined); - return this.local; - })); + this._onChange.fire(undefined); + return this.local; + }); } queryGallery(options: IQueryOptions = {}): Promise> { @@ -491,52 +484,19 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return Promise.resolve(this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), undefined, sideByside ? SIDE_GROUP : ACTIVE_GROUP)); } - private getDistinctInstalledExtensions(allInstalled: ILocalExtension[]): Promise { - if (!this.hasDuplicates(allInstalled)) { - return Promise.resolve(allInstalled); + private belongsToWindow(extension: ILocalExtension): boolean { + if (!this.extensionManagementServerService.remoteExtensionManagementServer) { + return true; } - return Promise.all([this.runtimeExtensionService.getExtensions(), this.extensionEnablementService.getDisabledExtensions()]) - .then(([runtimeExtensions, disabledExtensionIdentifiers]) => { - const groups = groupBy(allInstalled, (extension: ILocalExtension) => { - const isDisabled = disabledExtensionIdentifiers.some(identifier => areSameExtensions(identifier, extension.identifier)); - if (isDisabled) { - return extension.location.scheme === Schemas.file ? 'disabled:primary' : 'disabled:secondary'; - } else { - return 'enabled'; - } - }); - const enabled: ILocalExtension[] = []; - const notRunningExtensions: ILocalExtension[] = []; - const seenExtensions: { [id: string]: boolean } = Object.create({}); - for (const extension of (groups['enabled'] || [])) { - if (runtimeExtensions.some(r => r.extensionLocation.toString() === extension.location.toString())) { - enabled.push(extension); - seenExtensions[extension.identifier.id] = true; - } else { - notRunningExtensions.push(extension); - } - } - for (const extension of notRunningExtensions) { - if (!seenExtensions[extension.identifier.id]) { - enabled.push(extension); - seenExtensions[extension.identifier.id] = true; - } - } - const primaryDisabled = groups['disabled:primary'] || []; - const secondaryDisabled = (groups['disabled:secondary'] || []).filter(disabled => { - return primaryDisabled.every(p => !areSameExtensions(p.identifier, disabled.identifier)); - }); - return [...enabled, ...primaryDisabled, ...secondaryDisabled]; - }); - } - - private hasDuplicates(extensions: ILocalExtension[]): boolean { - const seen: { [key: string]: boolean; } = Object.create(null); - for (const i of extensions) { - if (seen[i.identifier.id]) { + const extensionManagementServer = this.extensionManagementServerService.getExtensionManagementServer(extension.location); + if (isUIExtension(extension.manifest, this.configurationService)) { + if (this.extensionManagementServerService.localExtensionManagementServer === extensionManagementServer) { + return true; + } + } else { + if (this.extensionManagementServerService.remoteExtensionManagementServer === extensionManagementServer) { return true; } - seen[i.identifier.id] = true; } return false; } @@ -548,13 +508,13 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, // Loading the compatible version only there is an engine property // Otherwise falling back to old way so that we will not make many roundtrips if (gallery.properties.engine) { - this.galleryService.loadCompatibleVersion(gallery) + this.galleryService.getCompatibleExtension(gallery) .then(compatible => compatible ? this.syncLocalWithGalleryExtension(result!, compatible) : null); } else { this.syncLocalWithGalleryExtension(result, gallery); } } else { - result = new Extension(this.galleryService, this.stateProvider, [], gallery, this.telemetryService, this.logService, this.fileService); + result = new Extension(this.galleryService, this.stateProvider, undefined, gallery, this.telemetryService, this.logService, this.fileService); } if (maliciousExtensionSet.has(result.identifier.id)) { @@ -581,13 +541,15 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, private syncLocalWithGalleryExtension(extension: Extension, gallery: IGalleryExtension) { // Sync the local extension with gallery extension if local extension doesnot has metadata - Promise.all(extension.locals.map(local => local.metadata ? Promise.resolve(local) : this.extensionService.updateMetadata(local, { id: gallery.identifier.uuid, publisherDisplayName: gallery.publisherDisplayName, publisherId: gallery.publisherId }))) - .then(locals => { - extension.locals = locals; - extension.gallery = gallery; - this._onChange.fire(extension); - this.eventuallyAutoUpdateExtensions(); - }); + if (extension.local) { + (extension.local.metadata ? Promise.resolve(extension.local) : this.extensionService.updateMetadata(extension.local, { id: gallery.identifier.uuid, publisherDisplayName: gallery.publisherDisplayName, publisherId: gallery.publisherId })) + .then(local => { + extension.local = local; + extension.gallery = gallery; + this._onChange.fire(extension); + this.eventuallyAutoUpdateExtensions(); + }); + } } checkForUpdates(): Promise { @@ -694,14 +656,10 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, } uninstall(extension: IExtension): Promise { - if (!(extension instanceof Extension)) { - return Promise.resolve(); - } + const ext = extension.local ? extension : this.installed.filter(e => areSameExtensions(e.identifier, extension.identifier))[0]; + const toUninstall: ILocalExtension | null = ext && ext.local ? ext.local : null; - const ext = extension as Extension; - const toUninstall: ILocalExtension[] = ext.locals.length ? ext.locals : this.installed.filter(e => areSameExtensions(e.identifier, extension.identifier))[0].locals; - - if (!toUninstall || !toUninstall.length) { + if (!toUninstall) { return Promise.reject(new Error('Missing local')); } @@ -709,8 +667,8 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return this.progressService.withProgress({ location: ProgressLocation.Extensions, title: nls.localize('uninstallingExtension', 'Uninstalling extension....'), - source: `${toUninstall[0].identifier.id}` - }, () => Promise.all(toUninstall.map(local => this.extensionService.uninstall(local))).then(() => undefined)); + source: `${toUninstall.identifier.id}` + }, () => this.extensionService.uninstall(toUninstall).then(() => undefined)); } installVersion(extension: IExtension, version: string): Promise { @@ -722,7 +680,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return Promise.reject(new Error('Missing gallery')); } - return this.galleryService.getExtension(extension.gallery.identifier, version) + return this.galleryService.getCompatibleExtension(extension.gallery.identifier, version) .then(gallery => { if (!gallery) { return undefined; @@ -739,21 +697,17 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, } reinstall(extension: IExtension): Promise { - if (!(extension instanceof Extension)) { - return Promise.resolve(); - } + const ext = extension.local ? extension : this.installed.filter(e => areSameExtensions(e.identifier, extension.identifier))[0]; + const toReinstall: ILocalExtension | null = ext && ext.local ? ext.local : null; - const ext = extension as Extension; - const toReinstall: ILocalExtension[] = ext.locals.length ? ext.locals : this.installed.filter(e => areSameExtensions(e.identifier, extension.identifier))[0].locals; - - if (!toReinstall || !toReinstall.length) { + if (!toReinstall) { return Promise.reject(new Error('Missing local')); } return this.progressService.withProgress({ location: ProgressLocation.Extensions, - source: `${toReinstall[0].identifier.id}` - }, () => Promise.all(toReinstall.map(local => this.extensionService.reinstallFromGallery(local))).then(() => undefined)); + source: `${toReinstall.identifier.id}` + }, () => this.extensionService.reinstallFromGallery(toReinstall).then(() => undefined)); } private installWithProgress(installTask: () => Promise, extensionName?: string): Promise { @@ -913,7 +867,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, let extension = this.installed.filter(e => areSameExtensions(e.identifier, gallery.identifier))[0]; if (!extension) { - extension = new Extension(this.galleryService, this.stateProvider, [], gallery, this.telemetryService, this.logService, this.fileService); + extension = new Extension(this.galleryService, this.stateProvider, undefined, gallery, this.telemetryService, this.logService, this.fileService); } this.installing.push(extension); @@ -924,29 +878,22 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, private onDidInstallExtension(event: DidInstallExtensionEvent): void { const { local, zipPath, error, gallery } = event; const installingExtension = gallery ? this.installing.filter(e => areSameExtensions(e.identifier, gallery.identifier))[0] : null; - let extension: Extension | undefined = installingExtension ? installingExtension : zipPath ? new Extension(this.galleryService, this.stateProvider, local ? [local] : [], undefined, this.telemetryService, this.logService, this.fileService) : undefined; + this.installing = installingExtension ? this.installing.filter(e => e !== installingExtension) : this.installing; + + if (local && !this.belongsToWindow(local)) { + return; + } + + let extension: Extension | undefined = installingExtension ? installingExtension : zipPath ? new Extension(this.galleryService, this.stateProvider, local, undefined, this.telemetryService, this.logService, this.fileService) : undefined; if (extension) { - this.installing = installingExtension ? this.installing.filter(e => e !== installingExtension) : this.installing; if (local) { const installed = this.installed.filter(e => areSameExtensions(e.identifier, extension!.identifier))[0]; if (installed) { extension = installed; - const newServer = this.extensionManagementServerService.getExtensionManagementServer(local.location); - const existingLocal = newServer && installed.locals.filter(l => { - const server = this.extensionManagementServerService.getExtensionManagementServer(l.location); - return server && server.authority === newServer.authority; - })[0]; - if (existingLocal) { - const locals = [...installed.locals]; - locals.splice(installed.locals.indexOf(existingLocal), 1, local); - installed.locals = locals; - } else { - installed.locals = [...installed.locals, local]; - } } else { - extension.locals = [local]; this.installed.push(extension); } + extension.local = local; } } this._onChange.fire(error ? undefined : extension); diff --git a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsActions.test.ts index 2a48e3dfcb9..281958141b7 100644 --- a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsActions.test.ts @@ -698,7 +698,7 @@ suite('ExtensionsActions Test', () => { }); test('Test EnableAction when there is no extension', () => { - const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction, []); + const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); assert.ok(!testObject.enabled); }); @@ -709,7 +709,7 @@ suite('ExtensionsActions Test', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction, []); + const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -723,7 +723,7 @@ suite('ExtensionsActions Test', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction, []); + const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); @@ -738,7 +738,7 @@ suite('ExtensionsActions Test', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction, []); + const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); @@ -751,7 +751,7 @@ suite('ExtensionsActions Test', () => { return instantiationService.get(IExtensionsWorkbenchService).queryGallery() .then(page => { - const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction, []); + const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); testObject.extension = page.firstPage[0]; assert.ok(!testObject.enabled); }); @@ -763,7 +763,7 @@ suite('ExtensionsActions Test', () => { return instantiationService.get(IExtensionsWorkbenchService).queryGallery() .then(page => { - const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction, []); + const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); testObject.extension = page.firstPage[0]; instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); @@ -778,7 +778,7 @@ suite('ExtensionsActions Test', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction, []); + const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); testObject.extension = extensions[0]; uninstallEvent.fire(local.identifier); assert.ok(!testObject.enabled); @@ -1214,6 +1214,7 @@ suite('ExtensionsActions Test', () => { return workbenchService.queryLocal().then(extensions => { testObject.extension = extensions[0]; return workbenchService.setEnablement(extensions[0], EnablementState.Disabled) + .then(() => testObject.update()) .then(() => { assert.ok(testObject.enabled); assert.equal('Please reload Visual Studio Code to complete the disabling of this extension.', testObject.tooltip); @@ -1250,6 +1251,7 @@ suite('ExtensionsActions Test', () => { .then(extensions => { testObject.extension = extensions[0]; return workbenchService.setEnablement(extensions[0], EnablementState.Enabled) + .then(() => testObject.update()) .then(() => { assert.ok(testObject.enabled); assert.equal('Please reload Visual Studio Code to complete the enabling of this extension.', testObject.tooltip); @@ -1294,6 +1296,7 @@ suite('ExtensionsActions Test', () => { installEvent.fire({ identifier: gallery.identifier, gallery }); didInstallEvent.fire({ identifier: gallery.identifier, gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }); return workbenchService.setEnablement(extensions[0], EnablementState.Enabled) + .then(() => testObject.update()) .then(() => { assert.ok(testObject.enabled); assert.equal('Please reload Visual Studio Code to complete the enabling of this extension.', testObject.tooltip); diff --git a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 38a85d5b399..2abd6bb2a40 100644 --- a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -12,7 +12,7 @@ import { IExtensionsWorkbenchService, ExtensionState, AutoCheckUpdatesConfigurat import { ExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/node/extensionsWorkbenchService'; import { IExtensionManagementService, IExtensionGalleryService, IExtensionEnablementService, IExtensionTipsService, ILocalExtension, IGalleryExtension, - DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IGalleryExtensionAssets, IExtensionIdentifier, EnablementState, InstallOperation + DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IGalleryExtensionAssets, IExtensionIdentifier, EnablementState, InstallOperation, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; @@ -37,6 +37,9 @@ import { URLService } from 'vs/platform/url/common/urlService'; import { URI } from 'vs/base/common/uri'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { ExtensionManagementServerService } from 'vs/workbench/services/extensions/node/extensionManagementServerService'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/node/remoteAgentService'; +import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -73,6 +76,9 @@ suite('ExtensionsWorkbenchServiceTest', () => { } }); + instantiationService.stub(IRemoteAgentService, RemoteAgentService); + instantiationService.stub(IExtensionManagementServerService, instantiationService.createInstance(ExtensionManagementServerService, { authority: 'vscode-local', extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local' })); + instantiationService.stub(IExtensionManagementService, ExtensionManagementService); instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event); instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event); diff --git a/src/vs/workbench/parts/feedback/electron-browser/feedback.ts b/src/vs/workbench/parts/feedback/electron-browser/feedback.ts index 405ea26a556..5d3f0f1f67b 100644 --- a/src/vs/workbench/parts/feedback/electron-browser/feedback.ts +++ b/src/vs/workbench/parts/feedback/electron-browser/feedback.ts @@ -429,12 +429,12 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { // Sentiment Buttons const inputActiveOptionBorderColor = theme.getColor(inputActiveOptionBorder); if (inputActiveOptionBorderColor) { - collector.addRule(`.monaco-shell .feedback-form .sentiment.checked { border: 1px solid ${inputActiveOptionBorderColor}; }`); + collector.addRule(`.monaco-workbench .feedback-form .sentiment.checked { border: 1px solid ${inputActiveOptionBorderColor}; }`); } // Links const linkColor = theme.getColor(buttonBackground) || theme.getColor(contrastBorder); if (linkColor) { - collector.addRule(`.monaco-shell .feedback-form .content .channels a { color: ${linkColor}; }`); + collector.addRule(`.monaco-workbench .feedback-form .content .channels a { color: ${linkColor}; }`); } }); \ No newline at end of file diff --git a/src/vs/workbench/parts/feedback/electron-browser/feedbackStatusbarItem.ts b/src/vs/workbench/parts/feedback/electron-browser/feedbackStatusbarItem.ts index 2b126ff033c..05db4b3c1db 100644 --- a/src/vs/workbench/parts/feedback/electron-browser/feedbackStatusbarItem.ts +++ b/src/vs/workbench/parts/feedback/electron-browser/feedbackStatusbarItem.ts @@ -168,6 +168,6 @@ class HideAction extends Action { registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); if (statusBarItemHoverBackground) { - collector.addRule(`.monaco-workbench > .part.statusbar > .statusbar-item .monaco-dropdown.send-feedback:hover { background-color: ${statusBarItemHoverBackground}; }`); + collector.addRule(`.monaco-workbench .part.statusbar > .statusbar-item .monaco-dropdown.send-feedback:hover { background-color: ${statusBarItemHoverBackground}; }`); } }); diff --git a/src/vs/workbench/parts/feedback/electron-browser/media/feedback.css b/src/vs/workbench/parts/feedback/electron-browser/media/feedback.css index 3c69f00d995..041ae4a8776 100644 --- a/src/vs/workbench/parts/feedback/electron-browser/media/feedback.css +++ b/src/vs/workbench/parts/feedback/electron-browser/media/feedback.css @@ -2,7 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-shell .feedback-form { + +.monaco-workbench .feedback-form { width: 420px; top: 30px; right: 6px; @@ -10,53 +11,53 @@ cursor: default; } -.monaco-shell .feedback-form h2 { +.monaco-workbench .feedback-form h2 { margin: 0; padding: 0; font-weight: normal; font-size: 1.8em; } -.monaco-shell .feedback-form h3 { +.monaco-workbench .feedback-form h3 { margin: 1em 0 0; padding: 0; font-weight: normal; font-size: 1.2em; } -.monaco-shell .feedback-form .content { +.monaco-workbench .feedback-form .content { font-size: 1.2em; } -.monaco-shell .feedback-form .content > div { +.monaco-workbench .feedback-form .content > div { display: inline-block; vertical-align: top; margin-top: 20px; } -.monaco-shell .feedback-form .content .contactus { +.monaco-workbench .feedback-form .content .contactus { padding: 10px; float: right; } -.monaco-shell .feedback-form .content .channels { +.monaco-workbench .feedback-form .content .channels { margin-top: 5px; font-size: 0.9em; } -.monaco-shell .char-counter { +.monaco-workbench .char-counter { padding-left: 3px; } -.monaco-shell .feedback-form .content .channels a { +.monaco-workbench .feedback-form .content .channels a { padding: 2px 0; } -.monaco-shell .feedback-form .content .channels a:hover { +.monaco-workbench .feedback-form .content .channels a:hover { text-decoration: underline; } -.monaco-shell .feedback-form .feedback-alias, .monaco-shell .feedback-form .feedback-description { +.monaco-workbench .feedback-form .feedback-alias, .monaco-workbench .feedback-form .feedback-description { resize: none; font-size: 1.1em; margin: 6px 0 0; @@ -65,25 +66,25 @@ box-sizing: border-box; } -.monaco-shell .feedback-form .feedback-empty { +.monaco-workbench .feedback-form .feedback-empty { margin-top: .2em; } -.monaco-shell .feedback-form .feedback-alias-checkbox { +.monaco-workbench .feedback-form .feedback-alias-checkbox { display: inline-block; vertical-align: text-top; margin-left: 0; } -.monaco-shell .feedback-form .feedback-alias { +.monaco-workbench .feedback-form .feedback-alias { height: 26px; } -.monaco-shell .feedback-form .feedback-alias:disabled { +.monaco-workbench .feedback-form .feedback-alias:disabled { opacity: 0.6; } -.monaco-shell .feedback-form .cancel { +.monaco-workbench .feedback-form .cancel { position: absolute; top: 0; right: 0; @@ -94,12 +95,12 @@ cursor: pointer; } -.monaco-shell .feedback-form .form-buttons { +.monaco-workbench .feedback-form .form-buttons { margin-top: 18px; text-align: right; } -.monaco-shell .feedback-form .sentiment { +.monaco-workbench .feedback-form .sentiment { height: 32px; width: 32px; display: inline-block; @@ -108,45 +109,45 @@ box-sizing: border-box; } -.monaco-shell .feedback-form .sentiment:hover { +.monaco-workbench .feedback-form .sentiment:hover { background-color: #eaeaea; } /* Statusbar */ -.monaco-shell .statusbar-item > .monaco-dropdown.send-feedback { +.monaco-workbench .statusbar-item > .monaco-dropdown.send-feedback { display: inline-block; } -.monaco-shell .statusbar-item > .monaco-dropdown.send-feedback > .dropdown-label.send-feedback { +.monaco-workbench .statusbar-item > .monaco-dropdown.send-feedback > .dropdown-label.send-feedback { -webkit-mask: url('smiley.svg') no-repeat 50% 50%; /* use mask to be able to change color dynamically */ width: 26px; } /* Theming */ -.monaco-shell.vs .feedback-form .feedback-alias, .monaco-shell.vs .feedback-form .feedback-description { +.vs .monaco-workbench .feedback-form .feedback-alias, .vs .monaco-workbench .feedback-form .feedback-description { font-family: inherit; border: 1px solid transparent; } -.monaco-shell.vs .feedback-form .cancel { +.vs .monaco-workbench .feedback-form .cancel { background: url('close.svg') center center no-repeat; } -.monaco-shell .feedback-form .form-buttons { +.monaco-workbench .feedback-form .form-buttons { display: flex; } -.monaco-shell .feedback-form .form-buttons .hide-button-container { +.monaco-workbench .feedback-form .form-buttons .hide-button-container { display: flex; } -.monaco-shell .feedback-form .form-buttons .hide-button-container input, -.monaco-shell .feedback-form .form-buttons .hide-button-container label { +.monaco-workbench .feedback-form .form-buttons .hide-button-container input, +.monaco-workbench .feedback-form .form-buttons .hide-button-container label { align-self: center; } -.monaco-shell .feedback-form .form-buttons .send { +.monaco-workbench .feedback-form .form-buttons .send { width: auto; background-image: url('twitter.svg'); background-position: 12px center; @@ -157,56 +158,56 @@ transition: width 200ms ease-out; } -.monaco-shell .feedback-form .form-buttons .send.in-progress, -.monaco-shell .feedback-form .form-buttons .send:hover { +.monaco-workbench .feedback-form .form-buttons .send.in-progress, +.monaco-workbench .feedback-form .form-buttons .send:hover { background-color: #006BB3; } -.monaco-shell .feedback-form .form-buttons .send:disabled { +.monaco-workbench .feedback-form .form-buttons .send:disabled { pointer-events: none; cursor: not-allowed; opacity: .65; } -.monaco-shell .feedback-form .form-buttons .send.success { +.monaco-workbench .feedback-form .form-buttons .send.success { background-color: #2d883e; } -.monaco-shell .feedback-form .form-buttons .send.error { +.monaco-workbench .feedback-form .form-buttons .send.error { background-color: #E51400; } -.monaco-shell.vs-dark .feedback-form h3 { +.vs-dark .monaco-workbench .feedback-form h3 { font-weight: normal; font-size: 1.2em; } -.monaco-shell.vs-dark .feedback-form .sentiment:hover { +.vs-dark .monaco-workbench .feedback-form .sentiment:hover { background-color: rgba(30,30,30,0.8); } -.monaco-shell.vs-dark .feedback-form .feedback-alias, .monaco-shell.vs-dark .feedback-form .feedback-description { +.vs-dark .monaco-workbench .feedback-form .feedback-alias, .vs-dark .monaco-workbench .feedback-form .feedback-description { font-family: inherit; } -.monaco-shell.vs-dark .feedback-form .cancel, -.monaco-shell.hc-black .feedback-form .cancel { +.vs-dark .monaco-workbench .feedback-form .cancel, +.hc-black .monaco-workbench .feedback-form .cancel { background: url('close-dark.svg') center center no-repeat; } -.monaco-shell .feedback-form .sentiment.smile { +.monaco-workbench .feedback-form .sentiment.smile { background-image: url('happy.svg'); background-position: center; background-repeat: no-repeat; } -.monaco-shell .feedback-form .sentiment.frown { +.monaco-workbench .feedback-form .sentiment.frown { background-image: url('sad.svg'); background-position: center; background-repeat: no-repeat; } -.monaco-shell .feedback-form .infotip { +.monaco-workbench .feedback-form .infotip { background-image: url('info.svg'); background-position: center; background-repeat: no-repeat; @@ -220,38 +221,38 @@ } /* High Contrast Theming */ -.monaco-shell.hc-black .feedback-form { +.hc-black .monaco-workbench .feedback-form { outline: 2px solid #6fc3df; outline-offset: -2px; } -.monaco-shell.hc-black .feedback-form .feedback-alias, .monaco-shell.hc-black .feedback-form .feedback-description { +.hc-black .monaco-workbench .feedback-form .feedback-alias, .hc-black .monaco-workbench .feedback-form .feedback-description { font-family: inherit; } -.monaco-shell.hc-black .feedback-form .content .contactus { +.hc-black .monaco-workbench .feedback-form .content .contactus { padding: 10px; float: right; } -.monaco-shell.hc-black .feedback-form .form-buttons .send, -.monaco-shell.hc-black .feedback-form .form-buttons .send.in-progress, -.monaco-shell.hc-black .feedback-form .form-buttons .send.success { +.hc-black .monaco-workbench .feedback-form .form-buttons .send, +.hc-black .monaco-workbench .feedback-form .form-buttons .send.in-progress, +.hc-black .monaco-workbench .feedback-form .form-buttons .send.success { background-color: #0C141F; color: #D4D4D4; border: 1px solid #6FC3DF; } -.monaco-shell.hc-black .feedback-form .form-buttons .send:hover { +.hc-black .monaco-workbench .feedback-form .form-buttons .send:hover { background-color: #0C141F; } -.monaco-shell .feedback-form .infotip { +.monaco-workbench .feedback-form .infotip { background: none; } -.monaco-shell .feedback-form .infotip:before { +.monaco-workbench .feedback-form .infotip:before { content: url('info.svg'); height: 16px; width: 16px; diff --git a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts index ac1fad843a4..09aaa5edc68 100644 --- a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts @@ -30,6 +30,7 @@ import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/group/ import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; /** * An implementation of editor for file system resources. @@ -39,6 +40,7 @@ export class TextFileEditor extends BaseTextEditor { static readonly ID = TEXT_FILE_EDITOR_ID; private restoreViewState: boolean; + private groupListener: IDisposable; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -96,7 +98,8 @@ export class TextFileEditor extends BaseTextEditor { // React to editors closing to preserve or clear view state. This needs to happen // in the onWillCloseEditor because at that time the editor has not yet // been disposed and we can safely persist the view state still as needed. - this._register((group as IEditorGroupView).onWillCloseEditor(e => { + this.groupListener = dispose(this.groupListener); + this.groupListener = ((group as IEditorGroupView).onWillCloseEditor(e => { if (e.editor === this.input) { this.doSaveOrClearTextEditorViewState(this.input); } @@ -276,4 +279,10 @@ export class TextFileEditor extends BaseTextEditor { this.saveTextEditorViewState(input.getResource()); } } + + dispose(): void { + this.groupListener = dispose(this.groupListener); + + super.dispose(); + } } diff --git a/src/vs/workbench/parts/files/browser/files.ts b/src/vs/workbench/parts/files/browser/files.ts index e6953ee8700..006ae8c00db 100644 --- a/src/vs/workbench/parts/files/browser/files.ts +++ b/src/vs/workbench/parts/files/browser/files.ts @@ -27,8 +27,11 @@ export function getResourceForCommand(resource: URI | object, listService: IList if (focused.length) { focus = focused[0]; } - } else { - focus = list.getFocus(); + } else if (list instanceof WorkbenchAsyncDataTree) { + const focused = list.getFocus(); + if (focused.length) { + focus = focused[0]; + } } if (focus instanceof ExplorerItem) { @@ -47,7 +50,8 @@ export function getMultiSelectedResources(resource: URI | object, listService: I // Explorer if (list instanceof WorkbenchAsyncDataTree) { const selection = list.getSelection().map((fs: ExplorerItem) => fs.resource); - const focus = list.getFocus(); + const focusedElements = list.getFocus(); + const focus = focusedElements.length ? focusedElements[0] : undefined; const mainUriStr = URI.isUri(resource) ? resource.toString() : focus instanceof ExplorerItem ? focus.resource.toString() : undefined; // If the resource is passed it has to be a part of the returned context. // We only respect the selection if it contains the focused element. diff --git a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts index 660e206385d..78c08b20065 100644 --- a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts @@ -140,7 +140,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { @memoize private get longDescription(): string { - return this.labelService.getUriLabel(resources.dirname(this.resource), { relative: true }); + return this.labelService.getUriLabel(resources.dirname(this.resource)); } getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string { diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index db6a66248c8..0bbd5075ca8 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -73,7 +73,7 @@ export class ExplorerModel implements IDisposable { } export class ExplorerItem { - public isDirectoryResolved: boolean; + private _isDirectoryResolved: boolean; public isError: boolean; constructor( @@ -86,7 +86,11 @@ export class ExplorerItem { private _mtime?: number, private _etag?: string, ) { - this.isDirectoryResolved = false; + this._isDirectoryResolved = false; + } + + get isDirectoryResolved(): boolean { + return this._isDirectoryResolved; } get isSymbolicLink(): boolean { @@ -157,7 +161,7 @@ export class ExplorerItem { // isDirectoryResolved is a very important indicator in the stat model that tells if the folder was fully resolved // the folder is fully resolved if either it has a list of children or the client requested this by using the resolveTo // array of resource path to resolve. - stat.isDirectoryResolved = !!raw.children || (!!resolveTo && resolveTo.some((r) => { + stat._isDirectoryResolved = !!raw.children || (!!resolveTo && resolveTo.some((r) => { return resources.isEqualOrParent(r, stat.resource); })); @@ -185,7 +189,7 @@ export class ExplorerItem { // Stop merging when a folder is not resolved to avoid loosing local data const mergingDirectories = disk.isDirectory || local.isDirectory; - if (mergingDirectories && local.isDirectoryResolved && !disk.isDirectoryResolved) { + if (mergingDirectories && local._isDirectoryResolved && !disk._isDirectoryResolved) { return; } @@ -194,13 +198,13 @@ export class ExplorerItem { local.updateName(disk.name); local._isDirectory = disk.isDirectory; local._mtime = disk.mtime; - local.isDirectoryResolved = disk.isDirectoryResolved; + local._isDirectoryResolved = disk._isDirectoryResolved; local._isSymbolicLink = disk.isSymbolicLink; local._isReadonly = disk.isReadonly; local.isError = disk.isError; // Merge Children if resolved - if (mergingDirectories && disk.isDirectoryResolved) { + if (mergingDirectories && disk._isDirectoryResolved) { // Map resource => stat const oldLocalChildren = new ResourceMap(); @@ -244,11 +248,11 @@ export class ExplorerItem { fetchChildren(fileService: IFileService): Promise { let promise: Promise = Promise.resolve(undefined); - if (!this.isDirectoryResolved) { + if (!this._isDirectoryResolved) { promise = fileService.resolveFile(this.resource, { resolveSingleChildDescendants: true }).then(stat => { const resolved = ExplorerItem.create(stat, this); ExplorerItem.mergeLocalWithDisk(resolved, this); - this.isDirectoryResolved = true; + this._isDirectoryResolved = true; }); } @@ -269,6 +273,11 @@ export class ExplorerItem { this.children.delete(this.getPlatformAwareName(child.name)); } + forgetChildren(): void { + this.children.clear(); + this._isDirectoryResolved = false; + } + private getPlatformAwareName(name: string): string { return (isLinux || !name) ? name : name.toLowerCase(); } diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index a9e24086881..10877a56430 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -73,12 +73,14 @@ const explorerViewletFocusId = 'explorerViewletFocus'; const explorerResourceIsFolderId = 'explorerResourceIsFolder'; const explorerResourceReadonly = 'explorerResourceReadonly'; const explorerResourceIsRootId = 'explorerResourceIsRoot'; +const explorerResourceCutId = 'explorerResourceCut'; export const ExplorerViewletVisibleContext = new RawContextKey(explorerViewletVisibleId, true); export const ExplorerFolderContext = new RawContextKey(explorerResourceIsFolderId, false); export const ExplorerResourceReadonlyContext = new RawContextKey(explorerResourceReadonly, false); export const ExplorerResourceNotReadonlyContext = ExplorerResourceReadonlyContext.toNegated(); export const ExplorerRootContext = new RawContextKey(explorerResourceIsRootId, false); +export const ExplorerResourceCut = new RawContextKey(explorerResourceCutId, false); export const FilesExplorerFocusedContext = new RawContextKey(filesExplorerFocusId, true); export const OpenEditorsVisibleContext = new RawContextKey(openEditorsVisibleId, false); export const OpenEditorsFocusedContext = new RawContextKey(openEditorsFocusId, true); diff --git a/src/vs/workbench/parts/files/electron-browser/explorerService.ts b/src/vs/workbench/parts/files/electron-browser/explorerService.ts index a439f87af54..8ac994aede6 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerService.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerService.ts @@ -156,7 +156,7 @@ export class ExplorerService implements IExplorerService { } refresh(): void { - this.model.roots.forEach(r => r.isDirectoryResolved = false); + this.model.roots.forEach(r => r.forgetChildren()); this._onDidChangeItem.fire(undefined); } @@ -249,7 +249,7 @@ export class ExplorerService implements IExplorerService { // Filter to the ones we care e = this.filterToViewRelevantEvents(e); const explorerItemChanged = (item: ExplorerItem) => { - item.isDirectoryResolved = false; + item.forgetChildren(); this._onDidChangeItem.fire(item); }; diff --git a/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts b/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts index 7c01ed22445..5d546ccb1a6 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts @@ -62,27 +62,27 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor let viewDescriptorsToDeregister: IViewDescriptor[] = []; const openEditorsViewDescriptor = this.createOpenEditorsViewDescriptor(); - const openEditorsViewDescriptorExists = viewDescriptors.some(v => v.id === openEditorsViewDescriptor.id); - const explorerViewDescriptor = this.createExplorerViewDescriptor(); - const explorerViewDescriptorExists = viewDescriptors.some(v => v.id === explorerViewDescriptor.id); - const emptyViewDescriptor = this.createEmptyViewDescriptor(); - const emptyViewDescriptorExists = viewDescriptors.some(v => v.id === emptyViewDescriptor.id); - - if (!openEditorsViewDescriptorExists) { + if (!viewDescriptors.some(v => v.id === openEditorsViewDescriptor.id)) { viewDescriptorsToRegister.push(openEditorsViewDescriptor); } + + const explorerViewDescriptor = this.createExplorerViewDescriptor(); + const registeredExplorerViewDescriptor = viewDescriptors.filter(v => v.id === explorerViewDescriptor.id)[0]; + const emptyViewDescriptor = this.createEmptyViewDescriptor(); + const registeredEmptyViewDescriptor = viewDescriptors.filter(v => v.id === emptyViewDescriptor.id)[0]; + if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY || this.workspaceContextService.getWorkspace().folders.length === 0) { - if (explorerViewDescriptorExists) { - viewDescriptorsToDeregister.push(explorerViewDescriptor); + if (registeredExplorerViewDescriptor) { + viewDescriptorsToDeregister.push(registeredExplorerViewDescriptor); } - if (!emptyViewDescriptorExists) { + if (!registeredEmptyViewDescriptor) { viewDescriptorsToRegister.push(emptyViewDescriptor); } } else { - if (emptyViewDescriptorExists) { - viewDescriptorsToDeregister.push(emptyViewDescriptor); + if (registeredEmptyViewDescriptor) { + viewDescriptorsToDeregister.push(registeredEmptyViewDescriptor); } - if (!explorerViewDescriptorExists) { + if (!registeredExplorerViewDescriptor) { viewDescriptorsToRegister.push(explorerViewDescriptor); } } diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts index 13a91fe33f6..1002950c754 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts @@ -15,7 +15,7 @@ import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/c import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; -import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext } from 'vs/workbench/parts/files/common/files'; +import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext, ExplorerResourceCut, IExplorerService } from 'vs/workbench/parts/files/common/files'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL } from 'vs/workbench/browser/actions/workspaceCommands'; import { CLOSE_SAVED_EDITORS_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { OPEN_FOLDER_SETTINGS_COMMAND, OPEN_FOLDER_SETTINGS_LABEL } from 'vs/workbench/parts/preferences/browser/preferencesActions'; @@ -24,6 +24,7 @@ import { ResourceContextKey } from 'vs/workbench/common/resources'; import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; // Contribute Global Actions const category = nls.localize('filesCategory', "File"); @@ -120,6 +121,17 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: pasteFileHandler }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'filesExplorer.cancelCut', + weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceCut), + primary: KeyCode.Escape, + handler: (accessor: ServicesAccessor) => { + const explorerService = accessor.get(IExplorerService); + explorerService.setToCopy([], true); + } +}); + const copyPathCommand = { id: COPY_PATH_COMMAND_ID, title: nls.localize('copyPath', "Copy Path") @@ -584,8 +596,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '6_close', command: { id: REVERT_FILE_COMMAND_ID, - title: nls.localize({ key: 'miRevert', comment: ['&& denotes a mnemonic'] }, "Re&&vert File"), - precondition: DirtyEditorContext + title: nls.localize({ key: 'miRevert', comment: ['&& denotes a mnemonic'] }, "Re&&vert File") }, order: 1 }); diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 05b535dbfc3..3c64e250710 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -7,7 +7,6 @@ import 'vs/css!./media/fileactions'; import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { isWindows, isLinux } from 'vs/base/common/platform'; -import { always } from 'vs/base/common/async'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; @@ -900,8 +899,8 @@ export function validateFileName(item: ExplorerItem, name: string): string { if (name !== item.name) { // Do not allow to overwrite existing file - const childExists = parent && !!parent.getChild(name); - if (childExists) { + const child = parent && parent.getChild(name); + if (child && child !== item) { return nls.localize('fileNameExistsError', "A file or folder **{0}** already exists at this location. Please choose a different name.", name); } } @@ -979,11 +978,9 @@ export class CompareWithClipboardAction extends Action { const name = resources.basename(resource); const editorLabel = nls.localize('clipboardComparisonLabel', "Clipboard ↔ {0}", name); - const cleanUp = () => { + return this.editorService.openEditor({ leftResource: resource.with({ scheme: CompareWithClipboardAction.SCHEME }), rightResource: resource, label: editorLabel }).finally(() => { this.registrationDisposal = dispose(this.registrationDisposal); - }; - - return always(this.editorService.openEditor({ leftResource: resource.with({ scheme: CompareWithClipboardAction.SCHEME }), rightResource: resource, label: editorLabel }), cleanUp); + }); } return Promise.resolve(true); diff --git a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts index 2ecfe05bd08..0bd9b93fcc5 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts @@ -463,12 +463,13 @@ CommandsRegistry.registerCommand({ const explorerView = viewlet.getExplorerView(); if (explorerView) { explorerView.setExpanded(true); - explorerService.select(uri, true).then(undefined, onUnexpectedError); + explorerService.select(uri, true).then(() => explorerView.focus(), onUnexpectedError); } } else { const openEditorsView = viewlet.getOpenEditorsView(); if (openEditorsView) { openEditorsView.setExpanded(true); + openEditorsView.focus(); } } }); diff --git a/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css b/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css index 7e059485bbf..746324d6769 100644 --- a/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css +++ b/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ /* Activity Bar */ -.monaco-workbench > .activitybar .monaco-action-bar .action-label.explore { +.monaco-workbench .activitybar .monaco-action-bar .action-label.explore { -webkit-mask: url('files-dark.svg') no-repeat 50% 50%; } diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 47eeceef1ad..67b3aa76db8 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -9,7 +9,7 @@ import * as perf from 'vs/base/common/performance'; import { sequence } from 'vs/base/common/async'; import { Action, IAction } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; -import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService } from 'vs/workbench/parts/files/common/files'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService, ExplorerResourceCut } from 'vs/workbench/parts/files/common/files'; import { NewFolderAction, NewFileAction, FileCopiedContext, RefreshExplorerView } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { toResource } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; @@ -122,6 +122,10 @@ export class ExplorerView extends ViewletPanel { return FileCopiedContext.bindTo(this.contextKeyService); } + @memoize private get resourceCutContextKey(): IContextKey { + return ExplorerResourceCut.bindTo(this.contextKeyService); + } + // Split view methods protected renderHeader(container: HTMLElement): void { @@ -143,8 +147,8 @@ export class ExplorerView extends ViewletPanel { setHeader(); } - protected layoutBody(size: number): void { - this.tree.layout(size); + protected layoutBody(height: number, width: number): void { + this.tree.layout(height, width); } renderBody(container: HTMLElement): void { @@ -155,7 +159,6 @@ export class ExplorerView extends ViewletPanel { this.toolbar.setActions(this.getActions(), this.getSecondaryActions())(); } - this.disposables.push(this.contextService.onDidChangeWorkbenchState(() => this.setTreeInput())); this.disposables.push(this.labelService.onDidChangeFormatters(() => { this._onDidChangeTitleArea.fire(); this.refresh(); @@ -246,7 +249,8 @@ export class ExplorerView extends ViewletPanel { const explorerLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer); this.disposables.push(explorerLabels); - const filesRenderer = this.instantiationService.createInstance(FilesRenderer, explorerLabels); + const updateWidth = (stat: ExplorerItem) => this.tree.updateWidth(stat); + const filesRenderer = this.instantiationService.createInstance(FilesRenderer, explorerLabels, updateWidth); this.disposables.push(filesRenderer); this.disposables.push(createFileIconThemableTreeContainerScope(container, this.themeService)); @@ -438,7 +442,18 @@ export class ExplorerView extends ViewletPanel { viewState = JSON.parse(rawViewState) as IAsyncDataTreeViewState; } + const previousInput = this.tree.getInput(); const promise = this.tree.setInput(input, viewState).then(() => { + if (Array.isArray(input)) { + if (!viewState || previousInput instanceof ExplorerItem) { + // There is no view state for this workspace, expand all roots. Or we transitioned from a folder workspace. + input.forEach(item => this.tree.expand(item).then(undefined, onUnexpectedError)); + } + if (Array.isArray(previousInput) && previousInput.length < input.length) { + // Roots added to the explorer -> expand them. + input.slice(previousInput.length).forEach(item => this.tree.expand(item).then(undefined, onUnexpectedError)); + } + } if (initialInputSetup) { perf.mark('didResolveExplorer'); } @@ -484,6 +499,7 @@ export class ExplorerView extends ViewletPanel { private onCopyItems(stats: ExplorerItem[], cut: boolean, previousCut: ExplorerItem[]): void { this.fileCopiedContextKey.set(stats.length > 0); + this.resourceCutContextKey.set(cut && stats.length > 0); if (previousCut) { previousCut.forEach(item => this.tree.refresh(item)); } diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index d2b763b4890..e9f412f022b 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -107,6 +107,7 @@ export class FilesRenderer implements ITreeRenderer void, @IContextViewService private readonly contextViewService: IContextViewService, @IThemeService private readonly themeService: IThemeService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -152,8 +153,7 @@ export class FilesRenderer implements ITreeRenderer { - // todo@isidor horizontal scrolling - // this.tree.updateWidth(stat); + this.updateWidth(stat); }); } diff --git a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts index 9f12476d434..06474606ccd 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts @@ -18,7 +18,7 @@ import { OpenEditorsFocusedContext, ExplorerFocusedContext, IFilesConfiguration, import { ITextFileService, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { CloseAllEditorsAction, CloseEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; -import { ToggleEditorLayoutAction } from 'vs/workbench/browser/actions/toggleEditorLayout'; +import { ToggleEditorLayoutAction } from 'vs/workbench/browser/actions/layoutActions'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -146,8 +146,11 @@ export class OpenEditorsView extends ViewletPanel { break; } case GroupChangeKind.EDITOR_OPEN: { - this.list.splice(index, 0, [new OpenEditor(e.editor, group)]); - setTimeout(() => this.updateSize(), this.structuralRefreshDelay); + setTimeout(() => { + this.list.splice(index, 0, [new OpenEditor(e.editor, group)]); + this.updateSize(); + this.focusActiveEditor(); + }, this.structuralRefreshDelay); break; } case GroupChangeKind.EDITOR_CLOSE: { @@ -304,9 +307,9 @@ export class OpenEditorsView extends ViewletPanel { return this.list; } - protected layoutBody(size: number): void { + protected layoutBody(height: number, width: number): void { if (this.list) { - this.list.layout(size); + this.list.layout(height, width); } } @@ -395,9 +398,11 @@ export class OpenEditorsView extends ViewletPanel { private focusActiveEditor(): void { if (this.list.length && this.editorGroupService.activeGroup) { const index = this.getIndex(this.editorGroupService.activeGroup, this.editorGroupService.activeGroup.activeEditor); - this.list.setFocus([index]); - this.list.setSelection([index]); - this.list.reveal(index); + if (index < this.list.length) { + this.list.setFocus([index]); + this.list.setSelection([index]); + this.list.reveal(index); + } } else { this.list.setFocus([]); this.list.setSelection([]); diff --git a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts index a5f20068a38..c7dc1ab2380 100644 --- a/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts +++ b/src/vs/workbench/parts/files/test/electron-browser/explorerModel.test.ts @@ -268,7 +268,7 @@ suite('Files - View Model', () => { const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, true, false, false, 'foo.html', Date.now(), d); merge2.removeChild(child); merge2.addChild(child); - merge2.isDirectoryResolved = true; + (merge2)._isDirectoryResolved = true; ExplorerItem.mergeLocalWithDisk(merge2, merge1); assert.strictEqual(merge1.getChild('foo.html').name, 'foo.html'); assert.deepEqual(merge1.getChild('foo.html').parent, merge1, 'Check parent'); diff --git a/src/vs/workbench/parts/markers/electron-browser/constants.ts b/src/vs/workbench/parts/markers/electron-browser/constants.ts index a9e0b9e8f6d..8d4348b87a7 100644 --- a/src/vs/workbench/parts/markers/electron-browser/constants.ts +++ b/src/vs/workbench/parts/markers/electron-browser/constants.ts @@ -16,6 +16,7 @@ export default { MARKERS_PANEL_SHOW_SINGLELINE_MESSAGE: 'problems.action.showSinglelineMessage', MARKER_OPEN_SIDE_ACTION_ID: 'problems.action.openToSide', MARKER_SHOW_PANEL_ID: 'workbench.action.showErrorsWarnings', + MARKER_SHOW_QUICK_FIX: 'problems.action.showQuickFixes', MarkerPanelFocusContextKey: new RawContextKey('problemsViewFocus', false), MarkerFocusContextKey: new RawContextKey('problemFocus', false), diff --git a/src/vs/workbench/parts/markers/electron-browser/markers.contribution.ts b/src/vs/workbench/parts/markers/electron-browser/markers.contribution.ts index 4afdc0bf6c3..765a7908f83 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markers.contribution.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markers.contribution.ts @@ -53,6 +53,20 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: Constants.MARKER_SHOW_QUICK_FIX, + weight: KeybindingWeight.WorkbenchContrib, + when: Constants.MarkerFocusContextKey, + primary: KeyMod.CtrlCmd | KeyCode.US_DOT, + handler: (accessor, args: any) => { + const markersPanel = (accessor.get(IPanelService).getActivePanel()); + const focusedElement = markersPanel.getFocusElement(); + if (focusedElement instanceof Marker) { + markersPanel.showQuickFixes(focusedElement); + } + } +}); + // configuration Registry.as(Extensions.Configuration).registerConfiguration({ 'id': 'problems', @@ -161,7 +175,7 @@ registerAction({ const panelService = accessor.get(IPanelService); const panel = panelService.getActivePanel(); if (panel instanceof MarkersPanel) { - panel.markersViewState.multiline = true; + panel.markersViewModel.multiline = true; } }, title: localize('show multiline', "Show message in multiple lines"), @@ -177,7 +191,7 @@ registerAction({ const panelService = accessor.get(IPanelService); const panel = panelService.getActivePanel(); if (panel instanceof MarkersPanel) { - panel.markersViewState.multiline = false; + panel.markersViewModel.multiline = false; } }, title: localize('show singleline', "Show message in single line"), diff --git a/src/vs/workbench/parts/markers/electron-browser/markers.ts b/src/vs/workbench/parts/markers/electron-browser/markers.ts index 6c8c060eefd..2ae1c928903 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markers.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markers.ts @@ -23,8 +23,7 @@ import { CodeAction } from 'vs/editor/common/modes'; import { Range } from 'vs/editor/common/core/range'; import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger'; -import { timeout } from 'vs/base/common/async'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { timeout, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; export const IMarkersWorkbenchService = createDecorator('markersWorkbenchService'); @@ -45,8 +44,8 @@ export class MarkersWorkbenchService extends Disposable implements IMarkersWorkb readonly markersModel: MarkersModel; - private readonly allFixesCache: Map> = new Map>(); - private readonly codeActionsPromises: Map>> = new Map>>(); + private readonly allFixesCache: Map> = new Map>(); + private readonly codeActionsPromises: Map>> = new Map>>(); private readonly codeActions: Map> = new Map>(); constructor( @@ -69,8 +68,16 @@ export class MarkersWorkbenchService extends Disposable implements IMarkersWorkb private onMarkerChanged(resources: URI[]): void { for (const resource of resources) { - this.allFixesCache.delete(resource.toString()); - this.codeActionsPromises.delete(resource.toString()); + const allFixes = this.allFixesCache.get(resource.toString()); + if (allFixes) { + allFixes.cancel(); + this.allFixesCache.delete(resource.toString()); + } + const codeActions = this.codeActionsPromises.get(resource.toString()); + if (codeActions) { + codeActions.forEach(promise => promise.cancel()); + this.codeActionsPromises.delete(resource.toString()); + } this.codeActions.delete(resource.toString()); this.markersModel.setResourceMarkers(resource, this.readMarkers(resource)); } @@ -93,7 +100,7 @@ export class MarkersWorkbenchService extends Disposable implements IMarkersWorkb } else { let codeActionsPromisesPerMarker = this.codeActionsPromises.get(marker.resource.toString()); if (!codeActionsPromisesPerMarker) { - codeActionsPromisesPerMarker = new Map>(); + codeActionsPromisesPerMarker = new Map>(); this.codeActionsPromises.set(marker.resource.toString(), codeActionsPromisesPerMarker); } if (!codeActionsPromisesPerMarker.has(markerKey)) { @@ -153,16 +160,18 @@ export class MarkersWorkbenchService extends Disposable implements IMarkersWorkb }, ACTIVE_GROUP).then(() => undefined); } - private getFixes(marker: Marker): Promise { + private getFixes(marker: Marker): CancelablePromise { return this._getFixes(marker.resource, new Range(marker.range.startLineNumber, marker.range.startColumn, marker.range.endLineNumber, marker.range.endColumn)); } - private async _getFixes(uri: URI, range?: Range): Promise { - const model = this.modelService.getModel(uri); - if (model) { - return getCodeActions(model, range ? range : model.getFullModelRange(), { type: 'manual', filter: { kind: CodeActionKind.QuickFix } }, CancellationToken.None /* TODO: use cancellation here */); - } - return []; + private _getFixes(uri: URI, range?: Range): CancelablePromise { + return createCancelablePromise(cancellationToken => { + const model = this.modelService.getModel(uri); + if (model) { + return getCodeActions(model, range ? range : model.getFullModelRange(), { type: 'manual', filter: { kind: CodeActionKind.QuickFix } }, cancellationToken); + } + return Promise.resolve([]); + }); } } diff --git a/src/vs/workbench/parts/markers/electron-browser/markersModel.ts b/src/vs/workbench/parts/markers/electron-browser/markersModel.ts index fec2e56c60d..cf207bf5e22 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersModel.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersModel.ts @@ -111,7 +111,7 @@ export class RelatedInformation { constructor( private resource: URI, - private marker: IMarker, + readonly marker: IMarker, readonly raw: IRelatedInformation ) { } } diff --git a/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts b/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts index bd0486b90ee..128c751cd08 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts @@ -14,7 +14,7 @@ import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/ import Constants from 'vs/workbench/parts/markers/electron-browser/constants'; import { Marker, ResourceMarkers, RelatedInformation, MarkersModel } from 'vs/workbench/parts/markers/electron-browser/markersModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MarkersFilterActionItem, MarkersFilterAction, QuickFixAction, QuickFixActionItem, IMarkersFilterActionChangeEvent, IMarkerFilterController } from 'vs/workbench/parts/markers/electron-browser/markersPanelActions'; +import { MarkersFilterActionItem, MarkersFilterAction, IMarkersFilterActionChangeEvent, IMarkerFilterController } from 'vs/workbench/parts/markers/electron-browser/markersPanelActions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import Messages from 'vs/workbench/parts/markers/electron-browser/messages'; import { RangeHighlightDecorations } from 'vs/workbench/browser/parts/editor/rangeDecorations'; @@ -33,7 +33,7 @@ import { IExpression, getEmptyExpression } from 'vs/base/common/glob'; import { mixin, deepClone } from 'vs/base/common/objects'; import { IWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { isAbsolute, join } from 'vs/base/common/paths'; -import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, TreeElement, MarkersTreeAccessibilityProvider, MarkersViewState } from 'vs/workbench/parts/markers/electron-browser/markersTreeViewer'; +import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, TreeElement, MarkersTreeAccessibilityProvider, MarkersViewModel } from 'vs/workbench/parts/markers/electron-browser/markersTreeViewer'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Separator, ActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; @@ -43,6 +43,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { domEvent } from 'vs/base/browser/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ResourceLabels } from 'vs/workbench/browser/labels'; +import { IMarker } from 'vs/platform/markers/common/markers'; function createModelIterator(model: MarkersModel): Iterator> { const resourcesIt = Iterator.fromArray(model.resourceMarkers); @@ -88,7 +89,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { private cachedFilterStats: { total: number; filtered: number; } | undefined = undefined; private currentResourceGotAddedToMarkersData: boolean = false; - readonly markersViewState: MarkersViewState; + readonly markersViewModel: MarkersViewModel; private disposables: IDisposable[] = []; constructor( @@ -108,8 +109,8 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { super(Constants.MARKERS_PANEL_ID, telemetryService, themeService, storageService); this.panelFoucusContextKey = Constants.MarkerPanelFocusContextKey.bindTo(contextKeyService); this.panelState = this.getMemento(StorageScope.WORKSPACE); - this.markersViewState = new MarkersViewState(this.panelState['multiline']); - this.markersViewState.onDidChangeViewState(this.onDidChangeViewState, this, this.disposables); + this.markersViewModel = instantiationService.createInstance(MarkersViewModel, this.panelState['multiline']); + this.markersViewModel.onDidChange(this.onDidChangeViewState, this, this.disposables); this.setCurrentActiveEditor(); } @@ -150,7 +151,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { public layout(dimension: dom.Dimension): void { this.treeContainer.style.height = `${dimension.height}px`; - this.tree.layout(dimension.height); + this.tree.layout(dimension.height, dimension.width); if (this.filterInputActionItem) { this.filterInputActionItem.toggleLayout(dimension.width < 1200); } @@ -177,10 +178,30 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { return this.actions; } + public showQuickFixes(marker: Marker): void { + const viewModel = this.markersViewModel.getViewModel(marker); + if (viewModel) { + viewModel.quickFixAction.run(); + } + } + public openFileAtElement(element: any, preserveFocus: boolean, sideByside: boolean, pinned: boolean): boolean { - const { resource, selection } = element instanceof Marker ? { resource: element.resource, selection: element.range } : - element instanceof RelatedInformation ? { resource: element.raw.resource, selection: element.raw } : { resource: null, selection: null }; + const { resource, selection, event, data } = element instanceof Marker ? { resource: element.resource, selection: element.range, event: 'problems.selectDiagnostic', data: this.getTelemetryData(element.marker) } : + element instanceof RelatedInformation ? { resource: element.raw.resource, selection: element.raw, event: 'problems.selectRelatedInformation', data: this.getTelemetryData(element.marker) } : { resource: null, selection: null, event: null, data: null }; if (resource && selection) { + /* __GDPR__ + "problems.selectDiagnostic" : { + "source": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, + "code" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + } + */ + /* __GDPR__ + "problems.selectRelatedInformation" : { + "source": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, + "code" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog(event, data); this.editorService.openEditor({ resource, options: { @@ -292,10 +313,10 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); - const virtualDelegate = new VirtualDelegate(this.markersViewState); + const virtualDelegate = new VirtualDelegate(this.markersViewModel); const renderers = [ this.instantiationService.createInstance(ResourceMarkersRenderer, this.treeLabels, onDidChangeRenderNodeCount.event), - this.instantiationService.createInstance(MarkerRenderer, this.markersViewState, a => this.getActionItem(a)), + this.instantiationService.createInstance(MarkerRenderer, this.markersViewModel), this.instantiationService.createInstance(RelatedInformationRenderer) ]; this.filter = new Filter(); @@ -336,6 +357,18 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { this._register(Event.debounce(markersNavigator.openResource, (last, event) => event, 75, true)(options => { this.openFileAtElement(options.element, options.editorOptions.preserveFocus, options.sideBySide, options.editorOptions.pinned); })); + this._register(this.tree.onDidChangeCollapseState(({ node }) => { + const { element } = node; + if (element instanceof RelatedInformation && !node.collapsed) { + /* __GDPR__ + "problems.expandRelatedInformation" : { + "source": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, + "code" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('problems.expandRelatedInformation', this.getTelemetryData(element.marker)); + } + })); this.tree.onContextMenu(this.onContextMenu, this, this._toDispose); @@ -382,13 +415,12 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { private onDidChangeModel(resources: URI[]) { for (const resource of resources) { + this.markersViewModel.remove(resource); const resourceMarkers = this.markersWorkbenchService.markersModel.getResourceMarkers(resource); if (resourceMarkers) { for (const marker of resourceMarkers.markers) { - this.markersViewState.add(marker); + this.markersViewModel.add(marker); } - } else { - this.markersViewState.remove(resource); } } this.currentResourceGotAddedToMarkersData = this.currentResourceGotAddedToMarkersData || this.isCurrentResourceGotAddedToMarkersData(resources); @@ -632,9 +664,6 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { this.filterInputActionItem = this.instantiationService.createInstance(MarkersFilterActionItem, this.filterAction, this); return this.filterInputActionItem; } - if (action.id === QuickFixAction.ID) { - return this.instantiationService.createInstance(QuickFixActionItem, action); - } return super.getActionItem(action); } @@ -668,11 +697,15 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { return { total, filtered }; } + private getTelemetryData({ source, code }: IMarker): any { + return { source, code }; + } + protected saveState(): void { this.panelState['filter'] = this.filterAction.filterText; this.panelState['filterHistory'] = this.filterAction.filterHistory; this.panelState['useFilesExclude'] = this.filterAction.useFilesExclude; - this.panelState['multiline'] = this.markersViewState.multiline; + this.panelState['multiline'] = this.markersViewModel.multiline; super.saveState(); } @@ -680,7 +713,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { public dispose(): void { super.dispose(); this.tree.dispose(); - this.markersViewState.dispose(); + this.markersViewModel.dispose(); this.disposables = dispose(this.disposables); } } \ No newline at end of file diff --git a/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts b/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts index b81abdf184c..278570fc877 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts @@ -29,7 +29,7 @@ import { Marker } from 'vs/workbench/parts/markers/electron-browser/markersModel import { IModelService } from 'vs/editor/common/services/modelService'; import { isEqual } from 'vs/base/common/resources'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import { FilterOptions } from 'vs/workbench/parts/markers/electron-browser/markersFilterOptions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -297,12 +297,16 @@ export class QuickFixAction extends Action { private updated: boolean = false; private disposables: IDisposable[] = []; + private _onShowQuickFixes: Emitter = new Emitter(); + readonly onShowQuickFixes: Event = this._onShowQuickFixes.event; + constructor( readonly marker: Marker, @IModelService modelService: IModelService, @IMarkersWorkbenchService private readonly markerWorkbenchService: IMarkersWorkbenchService, ) { super(QuickFixAction.ID, Messages.MARKERS_PANEL_ACTION_TOOLTIP_QUICKFIX, 'markers-panel-action-quickfix', false); + this.disposables.push(this._onShowQuickFixes); if (modelService.getModel(this.marker.resource)) { this.update(); } else { @@ -314,6 +318,11 @@ export class QuickFixAction extends Action { } } + run(): Promise { + this._onShowQuickFixes.fire(); + return Promise.resolve(); + } + private update(): void { if (!this.updated) { this.markerWorkbenchService.hasQuickFixes(this.marker).then(hasFixes => this.enabled = hasFixes); @@ -338,13 +347,17 @@ export class QuickFixActionItem extends ActionItem { public onClick(event: DOM.EventLike): void { DOM.EventHelper.stop(event, true); + this.showQuickFixes(); + } + + public showQuickFixes(): void { if (!this.element) { return; } const elementPosition = DOM.getDomNodePagePosition(this.element); this.markerWorkbenchService.getQuickFixActions((this.getAction()).marker).then(actions => { this.contextMenuService.showContextMenu({ - getAnchor: () => ({ x: elementPosition.left + 10, y: elementPosition.top + elementPosition.height }), + getAnchor: () => ({ x: elementPosition.left + 10, y: elementPosition.top + elementPosition.height + 4 }), getActions: () => actions }); }); diff --git a/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts b/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts index c18c2778053..a284d526c5e 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts @@ -16,8 +16,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ActionBar, IActionItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; -import { QuickFixAction } from 'vs/workbench/parts/markers/electron-browser/markersPanelActions'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { QuickFixAction, QuickFixActionItem } from 'vs/workbench/parts/markers/electron-browser/markersPanelActions'; import { ILabelService } from 'vs/platform/label/common/label'; import { dirname } from 'vs/base/common/resources'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; @@ -76,12 +76,12 @@ const enum TemplateId { export class VirtualDelegate implements IListVirtualDelegate { - constructor(private readonly markersViewState: MarkersViewState) { } + constructor(private readonly markersViewState: MarkersViewModel) { } getHeight(element: TreeElement): number { if (element instanceof Marker) { - const viewState = this.markersViewState.getViewState(element); - const noOfLines = !viewState || viewState.multiline ? element.lines.length : 1; + const viewModel = this.markersViewState.getViewModel(element); + const noOfLines = !viewModel || viewModel.multiline ? element.lines.length : 1; return noOfLines * 22; } return 22; @@ -201,8 +201,7 @@ export class FileResourceMarkersRenderer extends ResourceMarkersRenderer { export class MarkerRenderer implements ITreeRenderer { constructor( - private readonly markersViewState: MarkersViewState, - private actionItemProvider: IActionItemProvider, + private readonly markersViewState: MarkersViewModel, @IInstantiationService protected instantiationService: IInstantiationService ) { } @@ -210,7 +209,7 @@ export class MarkerRenderer implements ITreeRenderer action.id === QuickFixAction.ID ? instantiationService.createInstance(QuickFixActionItem, action) : null + })); this.icon = dom.append(parent, dom.$('.icon')); - this.multilineActionbar = this._register(new ActionBar(dom.append(parent, dom.$('.multiline-actions')), { actionItemProvider })); + this.multilineActionbar = this._register(new ActionBar(dom.append(parent, dom.$('.multiline-actions')))); this.messageAndDetailsContainer = dom.append(parent, dom.$('.marker-message-details')); this._register(toDisposable(() => this.disposables = dispose(this.disposables))); } @@ -254,38 +254,47 @@ class MarkerWidget extends Disposable { } dom.clearNode(this.messageAndDetailsContainer); - this.renderQuickfixActionbar(element); this.icon.className = 'marker-icon ' + MarkerWidget.iconClassNameFor(element.marker); + this.renderQuickfixActionbar(element); this.renderMultilineActionbar(element); this.renderMessageAndDetails(element, filterData); } private renderQuickfixActionbar(marker: Marker): void { - const quickFixAction = this.instantiationService.createInstance(QuickFixAction, marker); - this.actionBar.push([quickFixAction], { icon: true, label: false }); - dom.toggleClass(this.icon, 'quickFix', quickFixAction.enabled); - quickFixAction.onDidChange(({ enabled }) => { - if (!isUndefinedOrNull(enabled)) { - dom.toggleClass(this.icon, 'quickFix', enabled); - } - }, this, this.disposables); + const viewModel = this.markersViewModel.getViewModel(marker); + if (viewModel) { + const quickFixAction = viewModel.quickFixAction; + this.actionBar.push([quickFixAction], { icon: true, label: false }); + dom.toggleClass(this.icon, 'quickFix', quickFixAction.enabled); + quickFixAction.onDidChange(({ enabled }) => { + if (!isUndefinedOrNull(enabled)) { + dom.toggleClass(this.icon, 'quickFix', enabled); + } + }, this, this.disposables); + quickFixAction.onShowQuickFixes(() => { + const quickFixActionItem = this.actionBar.items[0]; + if (quickFixActionItem) { + quickFixActionItem.showQuickFixes(); + } + }, this, this.disposables); + } } private renderMultilineActionbar(marker: Marker): void { - const viewState = this.markersViewState.getViewState(marker); - const multiline = viewState && viewState.multiline; + const viewModel = this.markersViewModel.getViewModel(marker); + const multiline = viewModel && viewModel.multiline; const action = new Action('problems.action.toggleMultiline'); - action.enabled = viewState && marker.lines.length > 1; + action.enabled = viewModel && marker.lines.length > 1; action.tooltip = multiline ? localize('single line', "Show message in single line") : localize('multi line', "Show message in multiple lines"); action.class = multiline ? 'octicon octicon-chevron-up' : 'octicon octicon-chevron-down'; - action.run = () => { if (viewState) { viewState.multiline = !viewState.multiline; } return Promise.resolve(); }; + action.run = () => { if (viewModel) { viewModel.multiline = !viewModel.multiline; } return Promise.resolve(); }; this.multilineActionbar.push([action], { icon: true, label: false }); } private renderMessageAndDetails(element: Marker, filterData: MarkerFilterData) { const { marker, lines } = element; - const viewState = this.markersViewState.getViewState(element); + const viewState = this.markersViewModel.getViewModel(element); const multiline = !viewState || viewState.multiline; const lineMatches = filterData && filterData.lineMatches || []; const messageContainer = dom.append(this.messageAndDetailsContainer, dom.$('.marker-message')); @@ -461,10 +470,17 @@ export class Filter implements ITreeFilter { } } -export class MarkerViewState extends Disposable { +export class MarkerViewModel extends Disposable { - private readonly _onDidChangeViewState: Emitter = this._register(new Emitter()); - readonly onDidChangeViewState: Event = this._onDidChangeViewState.event; + private readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + constructor( + private readonly marker: Marker, + @IInstantiationService private instantiationService: IInstantiationService + ) { + super(); + } private _multiline: boolean = true; get multiline(): boolean { @@ -474,37 +490,48 @@ export class MarkerViewState extends Disposable { set multiline(value: boolean) { if (this._multiline !== value) { this._multiline = value; - this._onDidChangeViewState.fire(); + this._onDidChange.fire(); } } + + private _quickFixAction: QuickFixAction; + get quickFixAction(): QuickFixAction { + if (!this._quickFixAction) { + this._quickFixAction = this._register(this.instantiationService.createInstance(QuickFixAction, this.marker)); + } + return this._quickFixAction; + } } -export class MarkersViewState extends Disposable { +export class MarkersViewModel extends Disposable { - private readonly _onDidChangeViewState: Emitter = this._register(new Emitter()); - readonly onDidChangeViewState: Event = this._onDidChangeViewState.event; + private readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; - private readonly markersViewStates: Map = new Map(); + private readonly markersViewStates: Map = new Map(); private readonly markersPerResource: Map = new Map(); private bulkUpdate: boolean = false; - constructor(multiline: boolean = true) { + constructor( + multiline: boolean = true, + @IInstantiationService private instantiationService: IInstantiationService + ) { super(); this._multiline = multiline; } add(marker: Marker): void { if (!this.markersViewStates.has(marker.hash)) { - const disposables: IDisposable[] = []; - const viewState = new MarkerViewState(); - viewState.multiline = this.multiline; - viewState.onDidChangeViewState(() => { + const viewModel = this.instantiationService.createInstance(MarkerViewModel, marker); + const disposables: IDisposable[] = [viewModel]; + viewModel.multiline = this.multiline; + viewModel.onDidChange(() => { if (!this.bulkUpdate) { - this._onDidChangeViewState.fire(marker); + this._onDidChange.fire(marker); } }, this, disposables); - this.markersViewStates.set(marker.hash, { viewState, disposables }); + this.markersViewStates.set(marker.hash, { viewModel, disposables }); const markers = this.markersPerResource.get(marker.resource.toString()) || []; markers.push(marker); @@ -524,9 +551,9 @@ export class MarkersViewState extends Disposable { this.markersPerResource.delete(resource.toString()); } - getViewState(marker: Marker): MarkerViewState | null { + getViewModel(marker: Marker): MarkerViewModel | null { const value = this.markersViewStates.get(marker.hash); - return value ? value.viewState : null; + return value ? value.viewModel : null; } private _multiline: boolean = true; @@ -541,15 +568,15 @@ export class MarkersViewState extends Disposable { changed = true; } this.bulkUpdate = true; - this.markersViewStates.forEach(({ viewState }) => { - if (viewState.multiline !== value) { - viewState.multiline = value; + this.markersViewStates.forEach(({ viewModel }) => { + if (viewModel.multiline !== value) { + viewModel.multiline = value; changed = true; } }); this.bulkUpdate = false; if (changed) { - this._onDidChangeViewState.fire(undefined); + this._onDidChange.fire(undefined); } } diff --git a/src/vs/workbench/parts/performance/electron-browser/perfviewEditor.ts b/src/vs/workbench/parts/performance/electron-browser/perfviewEditor.ts index bfef17ef8ce..052ad299f86 100644 --- a/src/vs/workbench/parts/performance/electron-browser/perfviewEditor.ts +++ b/src/vs/workbench/parts/performance/electron-browser/perfviewEditor.ts @@ -141,6 +141,7 @@ class PerfModelContentProvider implements ITextModelContentProvider { const table: (string | number)[][] = []; table.push(['start => app.isReady', metrics.timers.ellapsedAppReady, '[main]', `initial startup: ${metrics.initialStartup}`]); table.push(['nls:start => nls:end', metrics.timers.ellapsedNlsGeneration, '[main]', `initial startup: ${metrics.initialStartup}`]); + table.push(['require(main.bundle.js)', metrics.initialStartup ? perf.getDuration('willLoadMainBundle', 'didLoadMainBundle') : undefined, '[main]', `initial startup: ${metrics.initialStartup}`]); table.push(['app.isReady => window.loadUrl()', metrics.timers.ellapsedWindowLoad, '[main]', `initial startup: ${metrics.initialStartup}`]); table.push(['require & init global storage', metrics.timers.ellapsedGlobalStorageInitMain, '[main]', `initial startup: ${metrics.initialStartup}`]); table.push(['window.loadUrl() => begin to require(workbench.main.js)', metrics.timers.ellapsedWindowLoadToRequire, '[main->renderer]', StartupKindToString(metrics.windowKind)]); diff --git a/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts index 3605aad7566..72e4cd23c34 100644 --- a/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts @@ -407,7 +407,12 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor private createList(parent: HTMLElement): void { this.keybindingsListContainer = DOM.append(parent, $('.keybindings-list-container')); this.keybindingsList = this._register(this.instantiationService.createInstance(WorkbenchList, this.keybindingsListContainer, new Delegate(), [new KeybindingItemRenderer(this, this.keybindingsService)], - { identityProvider: { getId: e => e.id }, ariaLabel: localize('keybindingsLabel', "Keybindings"), setRowLineHeight: false })) as WorkbenchList; + { + identityProvider: { getId: e => e.id }, + ariaLabel: localize('keybindingsLabel', "Keybindings"), + setRowLineHeight: false, + horizontalScrolling: false + })) as WorkbenchList; this._register(this.keybindingsList.onContextMenu(e => this.onContextMenu(e))); this._register(this.keybindingsList.onFocusChange(e => this.onFocusChange(e))); this._register(this.keybindingsList.onDidFocus(() => { diff --git a/src/vs/workbench/parts/preferences/browser/settingsTree.ts b/src/vs/workbench/parts/preferences/browser/settingsTree.ts index 37f37082112..ab40cc8e798 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsTree.ts @@ -12,7 +12,7 @@ import { alert as ariaAlert } from 'vs/base/browser/ui/aria/aria'; import { Button } from 'vs/base/browser/ui/button/button'; import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; -import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListVirtualDelegate, ListAriaRootRole } from 'vs/base/browser/ui/list/list'; import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IObjectTreeOptions, ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; @@ -257,6 +257,7 @@ export abstract class AbstractSettingRenderer implements ITreeRenderer(); readonly onDidClickOverrideElement: Event = this._onDidClickOverrideElement.event; @@ -385,6 +386,7 @@ export abstract class AbstractSettingRenderer implements ITreeRenderertemplate, onChange); - // Remove tree attributes - sometimes overridden by tree - should be managed there - template.containerElement.parentElement.parentElement.removeAttribute('role'); - template.containerElement.parentElement.parentElement.removeAttribute('aria-level'); - template.containerElement.parentElement.parentElement.removeAttribute('aria-posinset'); - template.containerElement.parentElement.parentElement.removeAttribute('aria-setsize'); } private renderDescriptionMarkdown(element: SettingsTreeSettingElement, text: string, disposeables: IDisposable[]): HTMLElement { @@ -1091,6 +1088,11 @@ export class SettingTreeRenderers { const settingElement = this.getSettingDOMElementForDOMElement(element); return settingElement && settingElement.getAttribute(AbstractSettingRenderer.SETTING_KEY_ATTR); } + + getIdForDOMElementInSetting(element: HTMLElement): string { + const settingElement = this.getSettingDOMElementForDOMElement(element); + return settingElement && settingElement.getAttribute(AbstractSettingRenderer.SETTING_ID_ATTR); + } } function renderValidations(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, calledOnStartup: boolean, originalAriaLabel: string) { @@ -1297,6 +1299,7 @@ export class SettingsTree extends ObjectTree { renderers, { supportDynamicHeights: true, + ariaRole: ListAriaRootRole.FORM, ariaLabel: localize('treeAriaLabel', "Settings"), identityProvider: { getId(e) { diff --git a/src/vs/workbench/parts/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/parts/preferences/browser/settingsTreeModels.ts index ff8557ac6c0..7deca62b6af 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsTreeModels.ts @@ -71,6 +71,8 @@ export class SettingsTreeNewExtensionsElement extends SettingsTreeElement { } export class SettingsTreeSettingElement extends SettingsTreeElement { + private static MAX_DESC_LINES = 20; + setting: ISetting; private _displayCategory: string; @@ -164,7 +166,13 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { } this.overriddenScopeList = overriddenScopeList; - this.description = this.setting.description.join('\n'); + if (this.setting.description.length > SettingsTreeSettingElement.MAX_DESC_LINES) { + const truncatedDescLines = this.setting.description.slice(0, SettingsTreeSettingElement.MAX_DESC_LINES); + truncatedDescLines.push('[...]'); + this.description = truncatedDescLines.join('\n'); + } else { + this.description = this.setting.description.join('\n'); + } if (this.setting.enum && (!this.setting.type || settingTypeEnumRenderable(this.setting.type))) { this.valueType = SettingValueType.Enum; diff --git a/src/vs/workbench/parts/preferences/browser/tocTree.ts b/src/vs/workbench/parts/preferences/browser/tocTree.ts index a30929be24b..72acc8e1417 100644 --- a/src/vs/workbench/parts/preferences/browser/tocTree.ts +++ b/src/vs/workbench/parts/preferences/browser/tocTree.ts @@ -126,15 +126,23 @@ class TOCTreeDelegate implements IListVirtualDelegate { } } -export function createTOCIterator(model: TOCTreeModel | SettingsTreeGroupElement): Iterator> { +export function createTOCIterator(model: TOCTreeModel | SettingsTreeGroupElement, tree: TOCTree): Iterator> { const groupChildren = model.children.filter(c => c instanceof SettingsTreeGroupElement); const groupsIt = Iterator.fromArray(groupChildren); + return Iterator.map(groupsIt, g => { + let nodeExists = true; + try { tree.getNode(g); } catch (e) { nodeExists = false; } + + const hasGroupChildren = g.children.some(c => c instanceof SettingsTreeGroupElement); + return { element: g, + collapsed: nodeExists ? undefined : true, + collapsible: hasGroupChildren, children: g instanceof SettingsTreeGroupElement ? - createTOCIterator(g) : + createTOCIterator(g, tree) : undefined }; }); diff --git a/src/vs/workbench/parts/preferences/electron-browser/media/settingsEditor2.css b/src/vs/workbench/parts/preferences/electron-browser/media/settingsEditor2.css index 5acd2d73baf..a29751e89bd 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/media/settingsEditor2.css +++ b/src/vs/workbench/parts/preferences/electron-browser/media/settingsEditor2.css @@ -113,7 +113,6 @@ text-decoration: underline; } -.settings-editor.no-toc-search > .settings-body .settings-tree-container .monaco-list-rows, .settings-editor.narrow-width > .settings-body .settings-tree-container .monaco-list-rows { margin-left: 0px; } diff --git a/src/vs/workbench/parts/preferences/electron-browser/settingsEditor2.ts b/src/vs/workbench/parts/preferences/electron-browser/settingsEditor2.ts index 5fd660b0669..9f52f2b6459 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/settingsEditor2.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/settingsEditor2.ts @@ -32,7 +32,7 @@ import { attachSuggestEnabledInputBoxStyler, SuggestEnabledInput } from 'vs/work import { SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; import { commonlyUsedData, tocData } from 'vs/workbench/parts/preferences/browser/settingsLayout'; import { AbstractSettingRenderer, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveExtensionsSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from 'vs/workbench/parts/preferences/browser/settingsTree'; -import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel } from 'vs/workbench/parts/preferences/browser/settingsTreeModels'; +import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/parts/preferences/browser/settingsTreeModels'; import { settingsTextInputBorder } from 'vs/workbench/parts/preferences/browser/settingsWidgets'; import { createTOCIterator, TOCTree, TOCTreeModel } from 'vs/workbench/parts/preferences/browser/tocTree'; import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, IPreferencesSearchService, ISearchProvider, MODIFIED_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/parts/preferences/common/preferences'; @@ -124,6 +124,9 @@ export class SettingsEditor2 extends BaseEditor { private editorMemento: IEditorMemento; + private tocFocusedElement: SettingsTreeGroupElement; + private settingsTreeScrollTop = 0; + constructor( @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -542,21 +545,22 @@ export class SettingsEditor2 extends BaseEditor { this.viewState)); this._register(this.tocTree.onDidChangeFocus(e => { - this.tocTree.setSelection(e.elements); const element: SettingsTreeGroupElement = e.elements[0]; + if (this.tocFocusedElement === e.elements[0]) { + return; + } + + this.tocFocusedElement = element; + this.tocTree.setSelection(e.elements); if (this.searchResultModel) { if (this.viewState.filterToCategory !== element) { this.viewState.filterToCategory = element; this.renderTree(); this.settingsTree.scrollTop = 0; } - } else if (element) { + } else if (element && (!e.browserEvent || !(e.browserEvent).fromScroll)) { this.settingsTree.reveal(element, 0); } - - // else if (element && (!e.payload || !e.payload.fromScroll)) { - // this.settingsTree.reveal(element, 0); - // } })); this._register(this.tocTree.onDidFocus(() => { @@ -605,13 +609,19 @@ export class SettingsEditor2 extends BaseEditor { this.settingRenderers.allRenderers)); this.settingsTree.getHTMLElement().attributes.removeNamedItem('tabindex'); - // Have to redefine role of the tree widget to form for input elements - // TODO:CDL make this an option for tree - this.settingsTree.getHTMLElement().setAttribute('role', 'form'); + this._register(this.settingsTree.onDidScroll(() => { + if (this.settingsTree.scrollTop === this.settingsTreeScrollTop) { + return; + } - // this._register(this.settingsTree.onDidScroll(() => { - // this.updateTreeScrollSync(); - // })); + this.settingsTreeScrollTop = this.settingsTree.scrollTop; + + // setTimeout because calling setChildren on the settingsTree can trigger onDidScroll, so it fires when + // setChildren has called on the settings tree but not the toc tree yet, so their rendered elements are out of sync + setTimeout(() => { + this.updateTreeScrollSync(); + }, 0); + })); } private notifyNoSaveNeeded() { @@ -646,37 +656,57 @@ export class SettingsEditor2 extends BaseEditor { return; } - // this.updateTreePagingByScroll(); + // Hack, see https://github.com/Microsoft/vscode/issues/64749 + const settingItems = this.settingsTree.getHTMLElement().querySelectorAll('.setting-item'); + const firstEl = settingItems[1] || settingItems[0]; + if (!firstEl) { + return; + } - const element = this.tocTreeModel.children[0]; - // const elementToSync = this.settingsTree.getFirstVisibleElement(); - // const element = elementToSync instanceof SettingsTreeSettingElement ? elementToSync.parent : - // elementToSync instanceof SettingsTreeGroupElement ? elementToSync : - // null; + const firstSettingId = this.settingRenderers.getIdForDOMElementInSetting(firstEl); + const elementToSync = this.settingsTreeModel.getElementById(firstSettingId); + const element = elementToSync instanceof SettingsTreeSettingElement ? elementToSync.parent : + elementToSync instanceof SettingsTreeGroupElement ? elementToSync : + null; if (element && this.tocTree.getSelection()[0] !== element) { - // this.tocTree.reveal(element); - // const elementTop = this.tocTree.getRelativeTop(element); - // collapseAll(this.tocTree, element); - // if (elementTop < 0 || elementTop > 1) { - // this.tocTree.reveal(element); - // } else { - // this.tocTree.reveal(element, elementTop); - // } + const ancestors = this.getAncestors(element); + ancestors.forEach(e => this.tocTree.expand(e)); - // this.tocTree.expand(element); + this.tocTree.reveal(element); + const elementTop = this.tocTree.getRelativeTop(element); + this.tocTree.collapseAll(); - // this.tocTree.setSelection([element]); - // this.tocTree.setFocus(element, { fromScroll: true }); + ancestors.forEach(e => this.tocTree.expand(e)); + if (elementTop < 0 || elementTop > 1) { + this.tocTree.reveal(element); + } else { + this.tocTree.reveal(element, elementTop); + } + + this.tocTree.expand(element); + + this.tocTree.setSelection([element]); + + const fakeKeyboardEvent = new KeyboardEvent('keydown'); + (fakeKeyboardEvent).fromScroll = true; + this.tocTree.setFocus([element], fakeKeyboardEvent); } } - // private updateTreePagingByScroll(): void { - // const lastVisibleElement = this.settingsTree.getLastVisibleElement(); - // if (lastVisibleElement && this.settingsTreeDataSource.pageTo(lastVisibleElement.index)) { - // this.renderTree(); - // } - // } + private getAncestors(element: SettingsTreeElement): SettingsTreeElement[] { + const ancestors: any[] = []; + + while (element.parent) { + if (element.parent.id !== 'root') { + ancestors.push(element.parent); + } + + element = element.parent; + } + + return ancestors.reverse(); + } private updateChangedSetting(key: string, value: any): Promise { // ConfigurationService displays the error if this fails. @@ -854,10 +884,11 @@ export class SettingsEditor2 extends BaseEditor { } else { this.settingsTreeModel = this.instantiationService.createInstance(SettingsTreeModel, this.viewState); this.settingsTreeModel.update(resolvedSettingsRoot); + this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.root as SettingsTreeGroupElement; + + this.refreshTOCTree(); this.refreshTree(); - this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.root as SettingsTreeGroupElement; - this.refreshTOCTree(); this.tocTree.collapseAll(); } @@ -936,7 +967,7 @@ export class SettingsEditor2 extends BaseEditor { } private refreshTOCTree(): void { - this.tocTree.setChildren(null, createTOCIterator(this.tocTreeModel)); + this.tocTree.setChildren(null, createTOCIterator(this.tocTreeModel, this.tocTree)); } private updateModifiedLabelForKey(key: string): void { @@ -991,7 +1022,6 @@ export class SettingsEditor2 extends BaseEditor { this.viewState.filterToCategory = null; this.tocTreeModel.currentSearchModel = this.searchResultModel; - this.refreshTOCTree(); this.onSearchModeToggled(); if (this.searchResultModel) { @@ -1006,6 +1036,8 @@ export class SettingsEditor2 extends BaseEditor { this.refreshTree(); this.renderResultCountMessages(); } + + this.refreshTOCTree(); } return Promise.resolve(null); @@ -1189,8 +1221,7 @@ export class SettingsEditor2 extends BaseEditor { const listHeight = dimension.height - (76 + 11 /* header height + padding*/); const settingsTreeHeight = listHeight - 14; this.settingsTreeContainer.style.height = `${settingsTreeHeight}px`; - this.settingsTree.layout(settingsTreeHeight); - this.settingsTree.layoutWidth(dimension.width); + this.settingsTree.layout(settingsTreeHeight, dimension.width); const tocTreeHeight = listHeight - 16; this.tocTreeContainer.style.height = `${tocTreeHeight}px`; diff --git a/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts b/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts index 83be58a4ade..21d823df728 100644 --- a/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts +++ b/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContributionsRegistry, IWorkbenchContribution, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWindowsService, IWindowService, IWindowsConfiguration } from 'vs/platform/windows/common/windows'; @@ -15,7 +15,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { RunOnceScheduler } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; import { isEqual } from 'vs/base/common/resources'; -import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { equals } from 'vs/base/common/objects'; @@ -24,7 +24,7 @@ interface IConfiguration extends IWindowsConfiguration { update: { channel: string; }; telemetry: { enableCrashReporter: boolean }; keyboard: { touchbar: { enabled: boolean } }; - workbench: { tree: { horizontalScrolling: boolean } }; + workbench: { tree: { horizontalScrolling: boolean }, useExperimentalGridLayout: boolean }; files: { useExperimentalFileWatcher: boolean, watcherExclude: object }; } @@ -38,14 +38,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo private enableCrashReporter: boolean; private touchbarEnabled: boolean; private treeHorizontalScrolling: boolean; - private windowsSmoothScrollingWorkaround: boolean; private experimentalFileWatcher: boolean; private fileWatcherExclude: object; - - private firstFolderResource?: URI; - private extensionHostRestarter: RunOnceScheduler; - - private onDidChangeWorkspaceFoldersUnbind: IDisposable; + private useGridLayout: boolean; constructor( @IWindowsService private readonly windowsService: IWindowsService, @@ -54,23 +49,11 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo @IEnvironmentService private readonly envService: IEnvironmentService, @IDialogService private readonly dialogService: IDialogService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IExtensionService private readonly extensionService: IExtensionService ) { super(); - const workspace = this.contextService.getWorkspace(); - this.firstFolderResource = workspace.folders.length > 0 ? workspace.folders[0].uri : undefined; - this.extensionHostRestarter = new RunOnceScheduler(() => this.extensionService.restartExtensionHost(), 10); - this.onConfigurationChange(configurationService.getValue(), false); - this.handleWorkbenchState(); - - this.registerListeners(); - } - - private registerListeners(): void { this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange(this.configurationService.getValue(), true))); - this._register(this.contextService.onDidChangeWorkbenchState(() => setTimeout(() => this.handleWorkbenchState()))); } private onConfigurationChange(config: IConfiguration, notify: boolean): void { @@ -138,9 +121,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo changed = true; } - // Windows: smooth scrolling workaround - if (isWindows && config.window && typeof config.window.smoothScrollingWorkaround === 'boolean' && config.window.smoothScrollingWorkaround !== this.windowsSmoothScrollingWorkaround) { - this.windowsSmoothScrollingWorkaround = config.window.smoothScrollingWorkaround; + // Workbench Grid Layout + if (config.workbench && typeof config.workbench.useExperimentalGridLayout === 'boolean' && config.workbench.useExperimentalGridLayout !== this.useGridLayout) { + this.useGridLayout = config.workbench.useExperimentalGridLayout; changed = true; } @@ -155,6 +138,55 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo } } + private doConfirm(message: string, detail: string, primaryButton: string, confirmed: () => void): void { + this.windowService.isFocused().then(focused => { + if (focused) { + return this.dialogService.confirm({ + type: 'info', + message, + detail, + primaryButton + }).then(res => { + if (res.confirmed) { + confirmed(); + } + }); + } + + return undefined; + }); + } +} + +export class WorkspaceChangeExtHostRelauncher extends Disposable implements IWorkbenchContribution { + + private firstFolderResource?: URI; + private extensionHostRestarter: RunOnceScheduler; + + private onDidChangeWorkspaceFoldersUnbind: IDisposable; + + constructor( + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IExtensionService extensionService: IExtensionService + ) { + super(); + + this.extensionHostRestarter = this._register(new RunOnceScheduler(() => extensionService.restartExtensionHost(), 10)); + + this.contextService.getCompleteWorkspace() + .then(workspace => { + this.firstFolderResource = workspace.folders.length > 0 ? workspace.folders[0].uri : undefined; + this.handleWorkbenchState(); + this._register(this.contextService.onDidChangeWorkbenchState(() => setTimeout(() => this.handleWorkbenchState()))); + }); + + this._register(toDisposable(() => { + if (this.onDidChangeWorkspaceFoldersUnbind) { + this.onDidChangeWorkspaceFoldersUnbind.dispose(); + } + })); + } + private handleWorkbenchState(): void { // React to folder changes when we are in workspace state @@ -187,26 +219,8 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo this.extensionHostRestarter.schedule(); // buffer calls to extension host restart } } - - private doConfirm(message: string, detail: string, primaryButton: string, confirmed: () => void): void { - this.windowService.isFocused().then(focused => { - if (focused) { - return this.dialogService.confirm({ - type: 'info', - message, - detail, - primaryButton - }).then(res => { - if (res.confirmed) { - confirmed(); - } - }); - } - - return undefined; - }); - } } const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(SettingsChangeRelauncher, LifecyclePhase.Restored); +workbenchRegistry.registerWorkbenchContribution(WorkspaceChangeExtHostRelauncher, LifecyclePhase.Restored); diff --git a/src/vs/workbench/parts/scm/electron-browser/dirtydiffDecorator.ts b/src/vs/workbench/parts/scm/electron-browser/dirtydiffDecorator.ts index a2683e8d6a8..d0a303be842 100644 --- a/src/vs/workbench/parts/scm/electron-browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/parts/scm/electron-browser/dirtydiffDecorator.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import 'vs/css!./media/dirtydiffDecorator'; -import { ThrottledDelayer, always, first } from 'vs/base/common/async'; +import { ThrottledDelayer, first } from 'vs/base/common/async'; import { IDisposable, dispose, toDisposable, Disposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import * as ext from 'vs/workbench/common/contributions'; @@ -1101,7 +1101,7 @@ export class DirtyDiffModel { }); }); - return always(this._originalURIPromise!, () => { + return this._originalURIPromise.finally(() => { this._originalURIPromise = undefined; }); } diff --git a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css index 84ad51a4b7b..848ee89aee3 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench > .activitybar > .content .monaco-action-bar .action-label.scm { +.monaco-workbench .activitybar > .content .monaco-action-bar .action-label.scm { -webkit-mask: url('icon-dark.svg') no-repeat 50% 50%; } diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 19d30856bec..2f7895892bb 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -724,6 +724,7 @@ function convertValidationType(type: InputValidationType): MessageType { export class RepositoryPanel extends ViewletPanel { private cachedHeight: number | undefined = undefined; + private cachedWidth: number | undefined = undefined; private inputBoxContainer: HTMLElement; private inputBox: InputBox; private listContainer: HTMLElement; @@ -910,7 +911,7 @@ export class RepositoryPanel extends ViewletPanel { } } - layoutBody(height: number = this.cachedHeight): void { + layoutBody(height: number = this.cachedHeight, width: number = this.cachedWidth): void { if (height === undefined) { return; } @@ -924,7 +925,7 @@ export class RepositoryPanel extends ViewletPanel { const editorHeight = this.inputBox.height; const listHeight = height - (editorHeight + 12 /* margin */); this.listContainer.style.height = `${listHeight}px`; - this.list.layout(listHeight); + this.list.layout(listHeight, width); toggleClass(this.inputBoxContainer, 'scroll', editorHeight >= 134); } else { @@ -932,7 +933,7 @@ export class RepositoryPanel extends ViewletPanel { removeClass(this.inputBoxContainer, 'scroll'); this.listContainer.style.height = `${height}px`; - this.list.layout(height); + this.list.layout(height, width); } } diff --git a/src/vs/workbench/parts/search/browser/media/searchview.css b/src/vs/workbench/parts/search/browser/media/searchview.css index 030d85f1cdf..9538b2816c4 100644 --- a/src/vs/workbench/parts/search/browser/media/searchview.css +++ b/src/vs/workbench/parts/search/browser/media/searchview.css @@ -177,6 +177,7 @@ line-height: 22px; display: flex; overflow: hidden; + padding-left: 22px; } .search-view .linematch > .match { @@ -390,3 +391,7 @@ .hc-black .monaco-workbench .search-view .linematch { line-height: 20px; } + +.vs .panel .search-view .monaco-inputbox { + border: 1px solid #ddd; +} diff --git a/src/vs/workbench/parts/search/browser/searchActions.ts b/src/vs/workbench/parts/search/browser/searchActions.ts index 965e0149c13..3dde8d15063 100644 --- a/src/vs/workbench/parts/search/browser/searchActions.ts +++ b/src/vs/workbench/parts/search/browser/searchActions.ts @@ -653,7 +653,7 @@ function matchToString(match: Match, indent = 0): string { const getFirstLinePrefix = () => `${match.range().startLineNumber},${match.range().startColumn}`; const getOtherLinePrefix = (i: number) => match.range().startLineNumber + i + ''; - const fullMatchLines = match.fullMatchText().split(/\r?\n/g); + const fullMatchLines = match.fullPreviewLines(); const largestPrefixSize = fullMatchLines.reduce((largest, _, i) => { const thisSize = i === 0 ? getFirstLinePrefix().length : diff --git a/src/vs/workbench/parts/search/browser/searchResultsView.ts b/src/vs/workbench/parts/search/browser/searchResultsView.ts index 20bec999903..499495c2387 100644 --- a/src/vs/workbench/parts/search/browser/searchResultsView.ts +++ b/src/vs/workbench/parts/search/browser/searchResultsView.ts @@ -10,7 +10,7 @@ import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { IAction } from 'vs/base/common/actions'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import * as nls from 'vs/nls'; @@ -26,11 +26,13 @@ import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction } from 'vs/workbench/parts/search/browser/searchActions'; import { SearchView } from 'vs/workbench/parts/search/browser/searchView'; import { FileMatch, FolderMatch, Match, RenderableMatch, SearchModel, BaseFolderMatch } from 'vs/workbench/parts/search/common/searchModel'; +import { createMatches } from 'vs/base/common/filters'; interface IFolderMatchTemplate { label: IResourceLabel; badge: CountBadge; actions: ActionBar; + disposables: IDisposable[]; } interface IFileMatchTemplate { @@ -38,6 +40,7 @@ interface IFileMatchTemplate { label: IResourceLabel; badge: CountBadge; actions: ActionBar; + disposables: IDisposable[]; } interface IMatchTemplate { @@ -87,14 +90,23 @@ export class FolderMatchRenderer extends Disposable implements ITreeRenderer, index: number, templateData: IFolderMatchTemplate): void { @@ -128,8 +140,7 @@ export class FolderMatchRenderer extends Disposable implements ITreeRenderer, index: number, templateData: IFileMatchTemplate): void { const fileMatch = node.element; templateData.el.setAttribute('data-resource', fileMatch.resource().toString()); - templateData.label.setFile(fileMatch.resource(), { hideIcon: false }); + templateData.label.setFile( + fileMatch.resource(), { + hideIcon: false, + matches: createMatches(node.filterData) + }); const count = fileMatch.count(); templateData.badge.setCount(count); templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchMatches', "{0} matches found", count) : nls.localize('searchMatch', "{0} match found", count)); @@ -181,8 +206,7 @@ export class FileMatchRenderer extends Disposable implements ITreeRenderer> { - const folderMatches = searchResult.folderMatches() - .filter(fm => !fm.isEmpty()) - .sort(searchMatchComparer); - - if (folderMatches.length === 1) { - return createFolderIterator(folderMatches[0], collapseResults); - } - - const foldersIt = Iterator.fromArray(folderMatches); - return Iterator.map(foldersIt, folderMatch => { - const children = createFolderIterator(folderMatch, collapseResults); - return >{ element: folderMatch, children }; - }); -} - -function createFolderIterator(folderMatch: FolderMatch, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterator> { - const filesIt = Iterator.fromArray( - folderMatch.matches() - .sort(searchMatchComparer)); - - return Iterator.map(filesIt, fileMatch => { - const children = createFileIterator(fileMatch); - - const collapsed = collapseResults === 'alwaysCollapse' || (fileMatch.matches().length > 10 && collapseResults !== 'alwaysExpand'); - - return >{ element: fileMatch, children, collapsed }; - }); -} - -function createFileIterator(fileMatch: FileMatch): Iterator> { - const matchesIt = Iterator.from( - fileMatch.matches() - .sort(searchMatchComparer)); - return Iterator.map(matchesIt, r => (>{ element: r })); -} - -export function createIterator(match: FolderMatch | FileMatch | SearchResult, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterator> { - return match instanceof SearchResult ? createResultIterator(match, collapseResults) : - match instanceof FolderMatch ? createFolderIterator(match, collapseResults) : - createFileIterator(match); -} - export class SearchView extends Viewlet implements IViewlet, IPanel { private static readonly MAX_TEXT_RESULTS = 10000; @@ -186,10 +145,11 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { @IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IThemeService protected themeService: IThemeService, + @IWorkbenchThemeService protected workbenchThemeService: IWorkbenchThemeService, @ISearchHistoryService private readonly searchHistoryService: ISearchHistoryService, @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IMenuService private readonly menuService: IMenuService + @IMenuService private readonly menuService: IMenuService, ) { super(VIEW_ID, configurationService, partService, telemetryService, themeService, storageService); @@ -483,21 +443,68 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { refreshTree(event?: IChangeEvent): void { const collapseResults = this.configurationService.getValue('search').collapseResults; if (!event || event.added || event.removed) { - this.tree.setChildren(null, createResultIterator(this.viewModel.searchResult, collapseResults)); + this.tree.setChildren(null, this.createResultIterator(collapseResults)); } else { event.elements.forEach(element => { if (element instanceof FolderMatch) { // The folder may or may not be in the tree. Refresh the whole thing. - this.tree.setChildren(null, createResultIterator(this.viewModel.searchResult, collapseResults)); + this.tree.setChildren(null, this.createResultIterator(collapseResults)); return; } const root = element instanceof SearchResult ? null : element; - this.tree.setChildren(root, createIterator(element, collapseResults)); + this.tree.setChildren(root, this.createIterator(element, collapseResults)); }); } } + private createResultIterator(collapseResults: ISearchConfigurationProperties['collapseResults']): Iterator> { + const folderMatches = this.searchResult.folderMatches() + .filter(fm => !fm.isEmpty()) + .sort(searchMatchComparer); + + if (folderMatches.length === 1) { + return this.createFolderIterator(folderMatches[0], collapseResults); + } + + const foldersIt = Iterator.fromArray(folderMatches); + return Iterator.map(foldersIt, folderMatch => { + const children = this.createFolderIterator(folderMatch, collapseResults); + return >{ element: folderMatch, children }; + }); + } + + private createFolderIterator(folderMatch: FolderMatch, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterator> { + const filesIt = Iterator.fromArray( + folderMatch.matches() + .sort(searchMatchComparer)); + + return Iterator.map(filesIt, fileMatch => { + const children = this.createFileIterator(fileMatch); + + let nodeExists = true; + try { this.tree.getNode(fileMatch); } catch (e) { nodeExists = false; } + + const collapsed = nodeExists ? undefined : + (collapseResults === 'alwaysCollapse' || (fileMatch.matches().length > 10 && collapseResults !== 'alwaysExpand')); + + return >{ element: fileMatch, children, collapsed }; + }); + } + + private createFileIterator(fileMatch: FileMatch): Iterator> { + const matchesIt = Iterator.from( + fileMatch.matches() + .sort(searchMatchComparer)); + return Iterator.map(matchesIt, r => (>{ element: r })); + } + + private createIterator(match: FolderMatch | FileMatch | SearchResult, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterator> { + return match instanceof SearchResult ? this.createResultIterator(collapseResults) : + match instanceof FolderMatch ? this.createFolderIterator(match, collapseResults) : + this.createFileIterator(match); + } + private replaceAll(): void { if (this.viewModel.searchResult.count() === 0) { return; @@ -611,6 +618,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { private createSearchResultsView(container: HTMLElement): void { this.resultsElement = dom.append(container, $('.results.show-file-icons')); const delegate = this.instantiationService.createInstance(SearchDelegate); + this._register(createFileIconThemableTreeContainerScope(this.resultsElement, this.workbenchThemeService)); const identityProvider: IIdentityProvider = { getId(element: RenderableMatch) { @@ -628,6 +636,15 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this._register(this.instantiationService.createInstance(MatchRenderer, this.viewModel, this)), ], { + keyboardNavigationLabelProvider: { + getKeyboardNavigationLabel: (element: RenderableMatch) => { + if (element instanceof FolderMatch || element instanceof FileMatch) { + return element.name(); + } + + return ''; + } + }, identityProvider, accessibilityProvider: this.instantiationService.createInstance(SearchAccessibilityProvider, this.viewModel) })); @@ -900,7 +917,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.resultsElement.style.height = searchResultContainerSize + 'px'; - this.tree.layout(searchResultContainerSize); + this.tree.layout(searchResultContainerSize, this.size.width); } layout(dimension: dom.Dimension): void { @@ -1241,13 +1258,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } private doSearch(query: ITextQuery, options: ITextQueryBuilderOptions, excludePatternText: string, includePatternText: string): Thenable { - // Progress total is 100.0% for more progress bar granularity - const progressTotal = 1000; - let progressWorked = 0; - - const progressRunner = query.useRipgrep ? - this.progressService.show(/*infinite=*/true) : - this.progressService.show(progressTotal); + const progressRunner = this.progressService.show(/*infinite=*/true); this.searchWidget.searchInput.clearMessage(); this.searching = true; @@ -1263,12 +1274,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.searching = false; // Complete up to 100% as needed - if (completed && !query.useRipgrep) { - progressRunner.worked(progressTotal - progressWorked); - setTimeout(() => progressRunner.done(), 200); - } else { - progressRunner.done(); - } + progressRunner.done(); // Do final render, then expand if just 1 file with less than 50 matches this.onSearchResultsChanged(); @@ -1370,13 +1376,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.searchWidget.searchInput.showMessage({ content: e.message, type: MessageType.ERROR }); this.viewModel.searchResult.clear(); - if (e.code === SearchErrorCode.unknownEncoding && !this.configurationService.getValue('search.useLegacySearch')) { - this.notificationService.prompt(Severity.Info, nls.localize('otherEncodingWarning', "You can enable \"search.useLegacySearch\" to search non-standard file encodings."), - [{ - label: nls.localize('otherEncodingWarning.openSettingsLabel', "Open Settings"), - run: () => this.openSettings('search.useLegacySearch') - }]); - } else if (e.code === SearchErrorCode.regexParseError && !this.configurationService.getValue('search.usePCRE2')) { + if (e.code === SearchErrorCode.regexParseError && !this.configurationService.getValue('search.usePCRE2')) { this.showPcre2Hint(); } @@ -1384,18 +1384,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } }; - let total: number = 0; - let worked: number = 0; let visibleMatches = 0; - const onProgress = (p: ISearchProgressItem) => { - // Progress - if ((p).total) { - total = (p).total; - } - if ((p).worked) { - worked = (p).worked; - } - }; // Handle UI updates in an interval to show frequent progress and results const uiRefreshHandle: any = setInterval(() => { @@ -1404,30 +1393,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { return; } - if (!query.useRipgrep) { - // Progress bar update - let fakeProgress = true; - if (total > 0 && worked > 0) { - const ratio = Math.round((worked / total) * progressTotal); - if (ratio > progressWorked) { // never show less progress than what we have already - progressRunner.worked(ratio - progressWorked); - progressWorked = ratio; - fakeProgress = false; - } - } - - // Fake progress up to 90%, or when actual progress beats it - const fakeMax = 900; - const fakeMultiplier = 12; - if (fakeProgress && progressWorked < fakeMax) { - // Linearly decrease the rate of fake progress. - // 1 is the smallest allowed amount of progress. - const fakeAmt = Math.round((fakeMax - progressWorked) / fakeMax * fakeMultiplier) || 1; - progressWorked += fakeAmt; - progressRunner.worked(fakeAmt); - } - } - // Search result tree update const fileCount = this.viewModel.searchResult.fileCount(); if (visibleMatches !== fileCount) { @@ -1441,7 +1406,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.searchWidget.setReplaceAllActionState(false); - return this.viewModel.search(query, onProgress) + return this.viewModel.search(query) .then(onComplete, onError); } diff --git a/src/vs/workbench/parts/search/common/queryBuilder.ts b/src/vs/workbench/parts/search/common/queryBuilder.ts index 979b7a2e4d9..035215bddeb 100644 --- a/src/vs/workbench/parts/search/common/queryBuilder.ts +++ b/src/vs/workbench/parts/search/common/queryBuilder.ts @@ -51,7 +51,6 @@ export interface ICommonQueryBuilderOptions { maxResults?: number; maxFileSize?: number; - useRipgrep?: boolean; disregardIgnoreFiles?: boolean; disregardGlobalIgnoreFiles?: boolean; disregardExcludeSettings?: boolean; @@ -150,11 +149,6 @@ export class QueryBuilder { folderResources.map(uri => this.getFolderQueryForRoot(uri, options, excludePattern))) .filter(query => !!query) as IFolderQuery[]; - // const useRipgrep = !folderResources || folderResources.every(folder => { - // const folderConfig = this.configurationService.getValue({ resource: folder }); - // return !folderConfig.search.useLegacySearch; - // }); - const queryProps: ICommonQueryProps = { _reason: options._reason, folderQueries, @@ -163,8 +157,7 @@ export class QueryBuilder { excludePattern: excludePattern.pattern, includePattern, - maxResults: options.maxResults, - useRipgrep: true + maxResults: options.maxResults }; // Filter extraFileResources against global include/exclude patterns - they are already expected to not belong to a workspace diff --git a/src/vs/workbench/parts/search/common/searchModel.ts b/src/vs/workbench/parts/search/common/searchModel.ts index f254431f6de..b700671faa0 100644 --- a/src/vs/workbench/parts/search/common/searchModel.ts +++ b/src/vs/workbench/parts/search/common/searchModel.ts @@ -135,6 +135,10 @@ export class Match { return thisMatchPreviewLines.join('\n'); } + fullPreviewLines(): string[] { + return this._fullPreviewLines.slice(this._fullPreviewRange.startLineNumber, this._fullPreviewRange.endLineNumber + 1); + } + getMatchString(): string { return this._oneLinePreviewText.substring(this._rangeInPreviewText.startColumn - 1, this._rangeInPreviewText.endColumn - 1); } @@ -960,7 +964,6 @@ export class SearchModel extends Disposable { "fileCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "options": { "${inline}": [ "${IPatternInfo}" ] }, "duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "useRipgrep": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "type" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "scheme" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } } @@ -970,7 +973,6 @@ export class SearchModel extends Disposable { fileCount: this._searchResult.fileCount(), options, duration, - useRipgrep: this._searchQuery.useRipgrep, type: stats && stats.type, scheme }); diff --git a/src/vs/workbench/parts/search/electron-browser/media/search.contribution.css b/src/vs/workbench/parts/search/electron-browser/media/search.contribution.css index eb5ed7fd15f..0fe89c6f881 100644 --- a/src/vs/workbench/parts/search/electron-browser/media/search.contribution.css +++ b/src/vs/workbench/parts/search/electron-browser/media/search.contribution.css @@ -4,6 +4,6 @@ *--------------------------------------------------------------------------------------------*/ /* Activity Bar */ -.monaco-workbench > .activitybar .monaco-action-bar .action-label.search { +.monaco-workbench .activitybar .monaco-action-bar .action-label.search { -webkit-mask: url('search-dark.svg') no-repeat 50% 50%; } \ No newline at end of file diff --git a/src/vs/workbench/parts/search/electron-browser/search.contribution.ts b/src/vs/workbench/parts/search/electron-browser/search.contribution.ts index 67263d4f954..a299f18b82a 100644 --- a/src/vs/workbench/parts/search/electron-browser/search.contribution.ts +++ b/src/vs/workbench/parts/search/electron-browser/search.contribution.ts @@ -618,11 +618,6 @@ configurationRegistry.registerConfiguration({ deprecationMessage: nls.localize('useRipgrepDeprecated', "Deprecated. Consider \"search.usePCRE2\" for advanced regex feature support."), default: true }, - 'search.useLegacySearch': { - type: 'boolean', - description: nls.localize('useLegacySearch', "Controls whether to use the deprecated legacy mode for text and file search. It supports some text encodings that are not supported by the standard ripgrep-based search."), - default: false - }, 'search.useIgnoreFiles': { type: 'boolean', markdownDescription: nls.localize('useIgnoreFiles', "Controls whether to use `.gitignore` and `.ignore` files when searching for files."), diff --git a/src/vs/workbench/parts/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/parts/search/test/browser/searchViewlet.test.ts index 8cfe81dba7f..24925478596 100644 --- a/src/vs/workbench/parts/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/parts/search/test/browser/searchViewlet.test.ts @@ -12,11 +12,8 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { IFileMatch, ITextSearchMatch, OneLineRange, QueryType } from 'vs/platform/search/common/search'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; -import { FileMatch, Match, SearchResult, RenderableMatch, searchMatchComparer } from 'vs/workbench/parts/search/common/searchModel'; +import { FileMatch, Match, searchMatchComparer, SearchResult } from 'vs/workbench/parts/search/common/searchModel'; import { TestContextService } from 'vs/workbench/test/workbenchTestServices'; -import { createIterator } from 'vs/workbench/parts/search/browser/searchView'; -import { ITreeElement } from 'vs/base/browser/ui/tree/tree'; -import { Iterator } from 'vs/base/common/iterator'; suite('Search - Viewlet', () => { let instantiation: TestInstantiationService; @@ -63,12 +60,6 @@ suite('Search - Viewlet', () => { assert.equal(fileMatch.id(), 'file:///c%3A/foo'); assert.equal(lineMatch.id(), 'file:///c%3A/foo>[2,1 -> 2,2]b'); - - const resultIterator = createIterator(result, 'auto'); - const first = resultIterator.next(); - - assert(!!first.value!.children); - assert.equal((>>first.value!.children).next().value!.element.id(), 'file:///c%3A/foo>[2,1 -> 2,2]b'); }); test('Comparer', () => { diff --git a/src/vs/workbench/parts/search/test/common/queryBuilder.test.ts b/src/vs/workbench/parts/search/test/common/queryBuilder.test.ts index a62178ae986..310e3b4d731 100644 --- a/src/vs/workbench/parts/search/test/common/queryBuilder.test.ts +++ b/src/vs/workbench/parts/search/test/common/queryBuilder.test.ts @@ -17,7 +17,7 @@ import { TestContextService, TestEnvironmentService } from 'vs/workbench/test/wo const DEFAULT_EDITOR_CONFIG = {}; const DEFAULT_USER_CONFIG = { useRipgrep: true, useIgnoreFiles: true, useGlobalIgnoreFiles: true }; -const DEFAULT_QUERY_PROPS = { useRipgrep: true }; +const DEFAULT_QUERY_PROPS = {}; const DEFAULT_TEXT_QUERY_PROPS = { usePCRE2: false }; suite('QueryBuilder', () => { diff --git a/src/vs/workbench/parts/search/test/common/searchModel.test.ts b/src/vs/workbench/parts/search/test/common/searchModel.test.ts index 5a8b8fdeb95..3e20387d40a 100644 --- a/src/vs/workbench/parts/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/parts/search/test/common/searchModel.test.ts @@ -50,7 +50,6 @@ suite('SearchModel', () => { resultCount: 1, type: 'searchProcess', detailStats: { - traversal: 'node', fileWalkTime: 0, cmdTime: 0, cmdResultCount: 0, diff --git a/src/vs/workbench/parts/search/test/common/searchResult.test.ts b/src/vs/workbench/parts/search/test/common/searchResult.test.ts index f374bea8d53..61421c17c10 100644 --- a/src/vs/workbench/parts/search/test/common/searchResult.test.ts +++ b/src/vs/workbench/parts/search/test/common/searchResult.test.ts @@ -40,6 +40,9 @@ suite('SearchResult', () => { assert.equal(lineMatch.range().startColumn, 1); assert.equal(lineMatch.range().endColumn, 4); assert.equal('file:///folder/file.txt>[2,1 -> 2,4]foo', lineMatch.id()); + + assert.equal(lineMatch.fullMatchText(), 'foo'); + assert.equal(lineMatch.fullMatchText(true), 'foo bar'); }); test('Line Match - Remove', function () { @@ -312,50 +315,6 @@ suite('SearchResult', () => { return voidPromise.then(() => assert.ok(testObject.isEmpty())); }); - //// ----- utils - //function lineHasDecorations(model: editor.IModel, lineNumber: number, decorations: { start: number; end: number; }[]): void { - // let lineDecorations:typeof decorations = []; - // let decs = model.getLineDecorations(lineNumber); - // for (let i = 0, len = decs.length; i < len; i++) { - // lineDecorations.push({ - // start: decs[i].range.startColumn, - // end: decs[i].range.endColumn - // }); - // } - // assert.deepEqual(lineDecorations, decorations); - //} - // - //function lineHasNoDecoration(model: editor.IModel, lineNumber: number): void { - // lineHasDecorations(model, lineNumber, []); - //} - // - //function lineHasDecoration(model: editor.IModel, lineNumber: number, start: number, end: number): void { - // lineHasDecorations(model, lineNumber, [{ - // start: start, - // end: end - // }]); - //} - //// ----- end utils - // - //test('Model Highlights', function () { - // - // let fileMatch = instantiation.createInstance(FileMatch, null, toUri('folder\\file.txt')); - // fileMatch.add(new Match(fileMatch, 'line2', 1, 0, 2)); - // fileMatch.connect(); - // lineHasDecoration(oneModel, 2, 1, 3); - //}); - // - //test('Dispose', () => { - // - // let fileMatch = instantiation.createInstance(FileMatch, null, toUri('folder\\file.txt')); - // fileMatch.add(new Match(fileMatch, 'line2', 1, 0, 2)); - // fileMatch.connect(); - // lineHasDecoration(oneModel, 2, 1, 3); - // - // fileMatch.dispose(); - // lineHasNoDecoration(oneModel, 2); - //}); - function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ITextSearchMatch[]): FileMatch { const rawMatch: IFileMatch = { resource: URI.file('/' + path), diff --git a/src/vs/workbench/parts/stats/node/workspaceStats.ts b/src/vs/workbench/parts/stats/node/workspaceStats.ts index d42cc242bf4..bed2040b456 100644 --- a/src/vs/workbench/parts/stats/node/workspaceStats.ts +++ b/src/vs/workbench/parts/stats/node/workspaceStats.ts @@ -311,12 +311,12 @@ export class WorkspaceStats implements IWorkbenchContribution { "workspace.py.azure-storage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-translator" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-iothub-device-client" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.py.azure-ml" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - "workspace.py.azure-cognitiveservices" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - "workspace.py.adal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - "workspace.py.pydocumentdb" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - "workspace.py.botbuilder-core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - "workspace.py.botbuilder-schema" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + "workspace.py.azure-ml" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-cognitiveservices" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.adal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.pydocumentdb" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.botbuilder-core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.botbuilder-schema" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.botframework-connector" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } } diff --git a/src/vs/workbench/parts/tasks/common/problemMatcher.ts b/src/vs/workbench/parts/tasks/common/problemMatcher.ts index fcb912367fc..a9889946c2e 100644 --- a/src/vs/workbench/parts/tasks/common/problemMatcher.ts +++ b/src/vs/workbench/parts/tasks/common/problemMatcher.ts @@ -20,6 +20,7 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { ExtensionsRegistry, ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { Event, Emitter } from 'vs/base/common/event'; export enum FileLocationKind { Auto, @@ -1092,7 +1093,8 @@ const problemPatternExtPoint = ExtensionsRegistry.registerExtensionPoint((resolve, reject) => { - problemPatternExtPoint.setHandler((extensions) => { + problemPatternExtPoint.setHandler((extensions, delta) => { // We get all statically know extension during startup in one batch try { - extensions.forEach(extension => { + delta.removed.forEach(extension => { + let problemPatterns = extension.value as Config.NamedProblemPatterns; + for (let pattern of problemPatterns) { + if (this.patterns[pattern.name]) { + delete this.patterns[pattern.name]; + } + } + }); + delta.added.forEach(extension => { let problemPatterns = extension.value as Config.NamedProblemPatterns; let parser = new ProblemPatternParser(new ExtensionRegistryReporter(extension.collector)); for (let pattern of problemPatterns) { @@ -1664,27 +1674,40 @@ const problemMatchersExtPoint = ExtensionsRegistry.registerExtensionPoint; get(name: string): NamedProblemMatcher; keys(): string[]; + readonly onMatcherChanged; } class ProblemMatcherRegistryImpl implements IProblemMatcherRegistry { private matchers: IStringDictionary; private readyPromise: Promise; + private _onMatchersChanged: Emitter = new Emitter(); + public get onMatcherChanged(): Event { return this._onMatchersChanged.event; } + constructor() { this.matchers = Object.create(null); this.fillDefaults(); this.readyPromise = new Promise((resolve, reject) => { - problemMatchersExtPoint.setHandler((extensions) => { + problemMatchersExtPoint.setHandler((extensions, delta) => { try { - extensions.forEach(extension => { + delta.removed.forEach(extension => { + let problemMatchers = extension.value; + for (let matcher of problemMatchers) { + if (this.matchers[matcher.name]) { + delete this.matchers[matcher.name]; + } + } + }); + delta.added.forEach(extension => { let problemMatchers = extension.value; let parser = new ProblemMatcherParser(new ExtensionRegistryReporter(extension.collector)); for (let matcher of problemMatchers) { @@ -1694,6 +1717,9 @@ class ProblemMatcherRegistryImpl implements IProblemMatcherRegistry { } } }); + if ((delta.removed.length > 0) || (delta.added.length > 0)) { + this._onMatchersChanged.fire(); + } } catch (error) { } let matcher = this.get('tsc-watch'); diff --git a/src/vs/workbench/parts/tasks/common/taskDefinitionRegistry.ts b/src/vs/workbench/parts/tasks/common/taskDefinitionRegistry.ts index 77200450f4e..ab8a3a4231e 100644 --- a/src/vs/workbench/parts/tasks/common/taskDefinitionRegistry.ts +++ b/src/vs/workbench/parts/tasks/common/taskDefinitionRegistry.ts @@ -74,7 +74,8 @@ const taskDefinitionsExtPoint = ExtensionsRegistry.registerExtensionPoint((resolve, reject) => { - taskDefinitionsExtPoint.setHandler((extensions) => { + taskDefinitionsExtPoint.setHandler((extensions, delta) => { try { - for (let extension of extensions) { + for (let extension of delta.removed) { + let taskTypes = extension.value; + for (let taskType of taskTypes) { + if (this.taskTypes && taskType.type && this.taskTypes[taskType.type]) { + delete this.taskTypes[taskType.type]; + } + } + } + for (let extension of delta.added) { let taskTypes = extension.value; for (let taskType of taskTypes) { let type = Configuration.from(taskType, extension.description.identifier, extension.collector); diff --git a/src/vs/workbench/parts/tasks/common/tasks.ts b/src/vs/workbench/parts/tasks/common/tasks.ts index 957987964dd..712309947a5 100644 --- a/src/vs/workbench/parts/tasks/common/tasks.ts +++ b/src/vs/workbench/parts/tasks/common/tasks.ts @@ -214,6 +214,11 @@ export interface PresentationOptions { * Controls whether to clear the terminal before executing the task. */ clear: boolean; + + /** + * Controls whether the task is executed in a specific terminal group using split panes. + */ + group?: string; } export namespace PresentationOptions { @@ -850,6 +855,7 @@ export class TaskSorter { } export const enum TaskEventKind { + DependsOnStarted = 'dependsOnStarted', Start = 'start', ProcessStarted = 'processStarted', Active = 'active', @@ -887,7 +893,7 @@ export const enum TaskRunSource { export namespace TaskEvent { export function create(kind: TaskEventKind.ProcessStarted | TaskEventKind.ProcessEnded, task: Task, processIdOrExitCode: number): TaskEvent; export function create(kind: TaskEventKind.Start, task: Task, terminalId?: number): TaskEvent; - export function create(kind: TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.Terminated | TaskEventKind.End, task: Task): TaskEvent; + export function create(kind: TaskEventKind.DependsOnStarted | TaskEventKind.Start | TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.Terminated | TaskEventKind.End, task: Task): TaskEvent; export function create(kind: TaskEventKind.Changed): TaskEvent; export function create(kind: TaskEventKind, task?: Task, processIdOrExitCodeOrTerminalId?: number): TaskEvent { if (task) { diff --git a/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts b/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts index fdc154f2ddb..1f39e52033a 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts @@ -105,7 +105,7 @@ const presentation: IJSONSchema = { enum: ['always', 'silent', 'never'], enumDescriptions: [ nls.localize('JsonSchema.tasks.presentation.reveal.always', 'Always reveals the terminal when this task is executed.'), - nls.localize('JsonSchema.tasks.presentation.reveal.silent', 'Only reveals the terminal if no problem matcher is associated with the task and an errors occurs executing it.'), + nls.localize('JsonSchema.tasks.presentation.reveal.silent', 'Only reveals the terminal if the task exits with an error.'), nls.localize('JsonSchema.tasks.presentation.reveal.never', 'Never reveals the terminal when this task is executed.'), ], default: 'always', @@ -126,7 +126,11 @@ const presentation: IJSONSchema = { type: 'boolean', default: false, description: nls.localize('JsonSchema.tasks.presentation.clear', 'Controls whether the terminal is cleared before executing the task.') - } + }, + group: { + type: 'string', + description: nls.localize('JsonSchema.tasks.presentation.group', 'Controls whether the task is executed in a specific terminal group using split panes.') + }, } }; @@ -337,6 +341,7 @@ let taskConfiguration: IJSONSchema = { description: nls.localize('JsonSchema.tasks.matchers', 'The problem matcher(s) to use. Can either be a string or a problem matcher definition or an array of strings and problem matchers.') }, runOptions: Objects.deepClone(runOptions), + dependsOn: Objects.deepClone(dependsOn), } }; @@ -508,7 +513,7 @@ Object.getOwnPropertyNames(definitions).forEach(key => { }); fixReferences(schema); -ProblemMatcherRegistry.onReady().then(() => { +export function updateProblemMatchers() { try { let matcherIds = ProblemMatcherRegistry.keys().map(key => '$' + key); definitions.problemMatcherType2.oneOf![0].enum = matcherIds; @@ -516,6 +521,10 @@ ProblemMatcherRegistry.onReady().then(() => { } catch (err) { console.log('Installing problem matcher ids failed'); } +} + +ProblemMatcherRegistry.onReady().then(() => { + updateProblemMatchers(); }); export default schema; diff --git a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts index 69888c34ea4..2f9570d8b9a 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -2665,7 +2665,7 @@ let schema: IJSONSchema = { }; import schemaVersion1 from './jsonSchema_v1'; -import schemaVersion2 from './jsonSchema_v2'; +import schemaVersion2, { updateProblemMatchers } from './jsonSchema_v2'; schema.definitions = { ...schemaVersion1.definitions, ...schemaVersion2.definitions, @@ -2674,3 +2674,8 @@ schema.oneOf = [...schemaVersion2.oneOf, ...schemaVersion1.oneOf]; let jsonRegistry = Registry.as(jsonContributionRegistry.Extensions.JSONContribution); jsonRegistry.registerSchema(schemaId, schema); + +ProblemMatcherRegistry.onMatcherChanged(() => { + updateProblemMatchers(); + jsonRegistry.notifySchemaChanged(schemaId); +}); diff --git a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts index df1507cefdd..54d58632d77 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts @@ -10,7 +10,7 @@ import * as Objects from 'vs/base/common/objects'; import * as Types from 'vs/base/common/types'; import * as Platform from 'vs/base/common/platform'; import * as Async from 'vs/base/common/async'; -import { IStringDictionary } from 'vs/base/common/collections'; +import { IStringDictionary, values } from 'vs/base/common/collections'; import { LinkedMap, Touch } from 'vs/base/common/map'; import Severity from 'vs/base/common/severity'; import { Event, Emitter } from 'vs/base/common/event'; @@ -42,6 +42,7 @@ import { interface TerminalData { terminal: ITerminalInstance; lastTask: string; + group?: string; } interface ActiveTerminalData { @@ -347,12 +348,13 @@ export class TerminalTaskSystem implements ITaskSystem { let promises: Promise[] = []; if (task.configurationProperties.dependsOn) { task.configurationProperties.dependsOn.forEach((dependency) => { - let task = resolver.resolve(dependency.workspaceFolder, dependency.task!); - if (task) { - let key = task.getMapKey(); + let dependencyTask = resolver.resolve(dependency.workspaceFolder, dependency.task!); + if (dependencyTask) { + let key = dependencyTask.getMapKey(); let promise = this.activeTasks[key] ? this.activeTasks[key].promise : undefined; if (!promise) { - promise = this.executeTask(task, resolver, trigger); + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.DependsOnStarted, task)); + promise = this.executeTask(dependencyTask, resolver, trigger); } promises.push(promise); } else { @@ -574,13 +576,16 @@ export class TerminalTaskSystem implements ITaskSystem { let key = task.getMapKey(); delete this.activeTasks[key]; this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Changed)); - switch (task.command.presentation!.panel) { - case PanelKind.Dedicated: - this.sameTaskTerminals[key] = terminal!.id.toString(); - break; - case PanelKind.Shared: - this.idleTaskTerminals.set(key, terminal!.id.toString(), Touch.AsOld); - break; + if (exitCode !== undefined) { + // Only keep a reference to the terminal if it is not being disposed. + switch (task.command.presentation!.panel) { + case PanelKind.Dedicated: + this.sameTaskTerminals[key] = terminal!.id.toString(); + break; + case PanelKind.Shared: + this.idleTaskTerminals.set(key, terminal!.id.toString(), Touch.AsOld); + break; + } } let reveal = task.command.presentation!.reveal; if ((reveal === RevealKind.Silent) && ((exitCode !== 0) || (watchingProblemMatcher.numberOfMatches > 0) && watchingProblemMatcher.maxMarkerSeverity && @@ -639,13 +644,16 @@ export class TerminalTaskSystem implements ITaskSystem { let key = task.getMapKey(); delete this.activeTasks[key]; this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Changed)); - switch (task.command.presentation!.panel) { - case PanelKind.Dedicated: - this.sameTaskTerminals[key] = terminal!.id.toString(); - break; - case PanelKind.Shared: - this.idleTaskTerminals.set(key, terminal!.id.toString(), Touch.AsOld); - break; + if (exitCode !== undefined) { + // Only keep a reference to the terminal if it is not being disposed. + switch (task.command.presentation!.panel) { + case PanelKind.Dedicated: + this.sameTaskTerminals[key] = terminal!.id.toString(); + break; + case PanelKind.Shared: + this.idleTaskTerminals.set(key, terminal!.id.toString(), Touch.AsOld); + break; + } } let reveal = task.command.presentation!.reveal; if (terminal && (reveal === RevealKind.Silent) && ((exitCode !== 0) || (startStopProblemMatcher.numberOfMatches > 0) && startStopProblemMatcher.maxMarkerSeverity && @@ -918,6 +926,7 @@ export class TerminalTaskSystem implements ITaskSystem { let prefersSameTerminal = presentationOptions.panel === PanelKind.Dedicated; let allowsSharedTerminal = presentationOptions.panel === PanelKind.Shared; + let group = presentationOptions.group; let taskKey = task.getMapKey(); let terminalToReuse: TerminalData | undefined; @@ -928,7 +937,20 @@ export class TerminalTaskSystem implements ITaskSystem { delete this.sameTaskTerminals[taskKey]; } } else if (allowsSharedTerminal) { - let terminalId = this.idleTaskTerminals.remove(taskKey) || this.idleTaskTerminals.shift(); + // Always allow to reuse the terminal previously used by the same task. + let terminalId = this.idleTaskTerminals.remove(taskKey); + if (!terminalId) { + // There is no idle terminal which was used by the same task. + // Search for any idle terminal used previously by a task of the same group + // (or, if the task has no group, a terminal used by a task without group). + for (const taskId of this.idleTaskTerminals.keys()) { + const idleTerminalId = this.idleTaskTerminals.get(taskId)!; + if (this.terminals[idleTerminalId].group === group) { + terminalId = this.idleTaskTerminals.remove(taskId); + break; + } + } + } if (terminalId) { terminalToReuse = this.terminals[terminalId]; } @@ -946,7 +968,26 @@ export class TerminalTaskSystem implements ITaskSystem { return [terminalToReuse.terminal, commandExecutable, undefined]; } - const result = this.terminalService.createTerminal(this.currentTask.shellLaunchConfig); + let result: ITerminalInstance | null = null; + if (group) { + // Try to find an existing terminal to split. + // Even if an existing terminal is found, the split can fail if the terminal width is too small. + for (const terminal of values(this.terminals)) { + if (terminal.group === group) { + const originalInstance = terminal.terminal; + const config = this.currentTask.shellLaunchConfig; + result = this.terminalService.splitInstance(originalInstance, config); + if (result) { + break; + } + } + } + } + if (!result) { + // Either no group is used, no terminal with the group exists or splitting an existing terminal failed. + result = this.terminalService.createTerminal(this.currentTask.shellLaunchConfig); + } + const terminalKey = result.id.toString(); result.onDisposed((terminal) => { let terminalData = this.terminals[terminalKey]; @@ -961,7 +1002,7 @@ export class TerminalTaskSystem implements ITaskSystem { delete this.activeTasks[task.getMapKey()]; } }); - this.terminals[terminalKey] = { terminal: result, lastTask: taskKey }; + this.terminals[terminalKey] = { terminal: result, lastTask: taskKey, group }; return [result, commandExecutable, undefined]; } diff --git a/src/vs/workbench/parts/tasks/node/taskConfiguration.ts b/src/vs/workbench/parts/tasks/node/taskConfiguration.ts index 23a661b1c9f..afb7a097daf 100644 --- a/src/vs/workbench/parts/tasks/node/taskConfiguration.ts +++ b/src/vs/workbench/parts/tasks/node/taskConfiguration.ts @@ -118,6 +118,11 @@ export interface PresentationOptionsConfig { * Controls whether the terminal should be cleared before running the task. */ clear?: boolean; + + /** + * Controls whether the task is executed in a specific terminal group using split panes. + */ + group?: string; } export interface RunOptionsConfig { @@ -794,7 +799,7 @@ namespace CommandOptions { namespace CommandConfiguration { export namespace PresentationOptions { - const properties: MetaData[] = [{ property: 'echo' }, { property: 'reveal' }, { property: 'focus' }, { property: 'panel' }, { property: 'showReuseMessage' }, { property: 'clear' }]; + const properties: MetaData[] = [{ property: 'echo' }, { property: 'reveal' }, { property: 'focus' }, { property: 'panel' }, { property: 'showReuseMessage' }, { property: 'clear' }, { property: 'group' }]; interface PresentationOptionsShape extends LegacyCommandProperties { presentation?: PresentationOptionsConfig; @@ -807,6 +812,7 @@ namespace CommandConfiguration { let panel: Tasks.PanelKind; let showReuseMessage: boolean; let clear: boolean; + let group: string | undefined; let hasProps = false; if (Types.isBoolean(config.echoCommand)) { echo = config.echoCommand; @@ -836,12 +842,15 @@ namespace CommandConfiguration { if (Types.isBoolean(presentation.clear)) { clear = presentation.clear; } + if (Types.isString(presentation.group)) { + group = presentation.group; + } hasProps = true; } if (!hasProps) { return undefined; } - return { echo: echo!, reveal: reveal!, focus: focus!, panel: panel!, showReuseMessage: showReuseMessage!, clear: clear! }; + return { echo: echo!, reveal: reveal!, focus: focus!, panel: panel!, showReuseMessage: showReuseMessage!, clear: clear!, group }; } export function assignProperties(target: Tasks.PresentationOptions, source: Tasks.PresentationOptions | undefined): Tasks.PresentationOptions | undefined { diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index a9254da93dd..ac884f34d06 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -235,7 +235,7 @@ export interface ITerminalService { setActiveInstance(terminalInstance: ITerminalInstance): void; setActiveInstanceByIndex(terminalIndex: number): void; getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance; - splitInstance(instance: ITerminalInstance, shell?: IShellLaunchConfig): void; + splitInstance(instance: ITerminalInstance, shell?: IShellLaunchConfig): ITerminalInstance | null; getActiveTab(): ITerminalTab | null; setActiveTabToNext(): void; @@ -418,11 +418,6 @@ export interface ITerminalInstance { */ readonly commandTracker: ITerminalCommandTracker; - /** - * The cwd that the terminal instance was initialized with. - */ - readonly initialCwd: string; - /** * Dispose the terminal instance, removing it from the panel/service and freeing up resources. * @@ -620,6 +615,7 @@ export interface ITerminalInstance { toggleEscapeSequenceLogging(): void; + getInitialCwd(): Promise; getCwd(): Promise; } @@ -636,7 +632,6 @@ export interface ITerminalProcessManager extends IDisposable { readonly processState: ProcessState; readonly ptyProcessReady: Promise; readonly shellProcessId: number; - readonly initialCwd: string; readonly onProcessReady: Event; readonly onProcessData: Event; @@ -648,6 +643,9 @@ export interface ITerminalProcessManager extends IDisposable { createProcess(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number); write(data: string): void; setDimensions(cols: number, rows: number): void; + + getInitialCwd(): Promise; + getCwd(): Promise; } export const enum ProcessState { @@ -677,10 +675,14 @@ export interface ITerminalProcessExtHostProxy extends IDisposable { emitTitle(title: string): void; emitPid(pid: number): void; emitExit(exitCode: number): void; + emitInitialCwd(initialCwd: string): void; + emitCwd(cwd: string): void; onInput: Event; onResize: Event<{ cols: number, rows: number }>; onShutdown: Event; + onRequestInitialCwd: Event; + onRequestCwd: Event; } export interface ITerminalProcessExtHostRequest { diff --git a/src/vs/workbench/parts/terminal/common/terminalService.ts b/src/vs/workbench/parts/terminal/common/terminalService.ts index 447418f27f5..9728d8318fa 100644 --- a/src/vs/workbench/parts/terminal/common/terminalService.ts +++ b/src/vs/workbench/parts/terminal/common/terminalService.ts @@ -264,21 +264,23 @@ export abstract class TerminalService implements ITerminalService { this.setActiveTabByIndex(newIndex); } - public splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig: IShellLaunchConfig = {}): void { + public splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig: IShellLaunchConfig = {}): ITerminalInstance | null { const tab = this._getTabForInstance(instanceToSplit); if (!tab) { - return; + return null; } const instance = tab.split(this._terminalFocusContextKey, this.configHelper, shellLaunchConfig); - if (instance) { - this._initInstanceListeners(instance); - this._onInstancesChanged.fire(); - - this._terminalTabs.forEach((t, i) => t.setVisible(i === this._activeTabIndex)); - } else { + if (!instance) { this._showNotEnoughSpaceToast(); + return null; } + + this._initInstanceListeners(instance); + this._onInstancesChanged.fire(); + + this._terminalTabs.forEach((t, i) => t.setVisible(i === this._activeTabIndex)); + return instance; } protected _initInstanceListeners(instance: ITerminalInstance): void { diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts index e016e1e90f5..fb600958c8b 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts @@ -285,12 +285,12 @@ configurationRegistry.registerConfiguration({ 'terminal.integrated.windowsEnableConpty': { description: nls.localize('terminal.integrated.windowsEnableConpty', "Whether to use ConPTY for Windows terminal process communication (requires Windows 10 build number 18309+). Winpty will be used if this is false."), type: 'boolean', - default: true + default: false } } }); -registerSingleton(ITerminalService, TerminalService); +registerSingleton(ITerminalService, TerminalService, true); (Registry.as(panel.Extensions.Panels)).registerPanel(new panel.PanelDescriptor( TerminalPanel, diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts index c77fbe34407..ac30cb45070 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts @@ -30,15 +30,19 @@ import { Command } from 'vs/editor/browser/editorExtensions'; import { timeout } from 'vs/base/common/async'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; +import { URI } from 'vs/base/common/uri'; export const TERMINAL_PICKER_PREFIX = 'term '; -function getCwdForSplit(configHelper: ITerminalConfigHelper, instance: ITerminalInstance, folders?: IWorkspaceFolder[], commandService?: ICommandService): Promise { +function getCwdForSplit(configHelper: ITerminalConfigHelper, instance: ITerminalInstance, folders?: IWorkspaceFolder[], commandService?: ICommandService): Promise { switch (configHelper.config.splitCwd) { - case 'workspaceRoot': { - // allow original behavior - let pathPromise: Promise = Promise.resolve(''); - if (folders.length > 1) { + case 'workspaceRoot': + let pathPromise: Promise; + if (folders.length === 0) { + pathPromise = Promise.resolve(''); + } else if (folders.length === 1) { + pathPromise = Promise.resolve(folders[0].uri); + } else if (folders.length > 1) { // Only choose a path when there's more than 1 folder const options: IPickOptions = { placeHolder: nls.localize('workbench.action.terminal.newWorkspacePlaceholder', "Select current working directory for new terminal") @@ -48,20 +52,14 @@ function getCwdForSplit(configHelper: ITerminalConfigHelper, instance: ITerminal // Don't split the instance if the workspace picker was canceled return undefined; } - return Promise.resolve(workspace.uri.fsPath); + return Promise.resolve(workspace.uri); }); } - return pathPromise; - } - case 'initial': { - return new Promise(resolve => { - resolve(instance.initialCwd); - }); - } - case 'inherited': { + case 'initial': + return instance.getInitialCwd(); + case 'inherited': return instance.getCwd(); - } } } @@ -385,7 +383,6 @@ export class SplitTerminalAction extends Action { if (!instance) { return Promise.resolve(undefined); } - return getCwdForSplit(this._terminalService.configHelper, instance, this.workspaceContextService.getWorkspace().folders, this.commandService).then(cwd => { if (cwd || (cwd === '')) { this._terminalService.splitInstance(instance, { cwd }); diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index 0949fabe74a..07d3ee2389d 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as browser from 'vs/base/browser/browser'; import * as lifecycle from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import * as platform from 'vs/base/common/platform'; @@ -31,12 +32,11 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/parts/terminal/common/terminalColorRegistry'; import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { INotificationService, Severity, IPromptChoice } from 'vs/platform/notification/common/notification'; import { ILogService } from 'vs/platform/log/common/log'; import { TerminalCommandTracker } from 'vs/workbench/parts/terminal/node/terminalCommandTracker'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { execFile, exec } from 'child_process'; +import { execFile } from 'child_process'; import { TERMINAL_COMMAND_ID } from 'vs/workbench/parts/terminal/common/terminalCommands'; import { TerminalProcessManager } from 'vs/workbench/parts/terminal/electron-browser/terminalProcessManager'; @@ -399,7 +399,6 @@ export class TerminalInstance implements ITerminalInstance { Terminal.strings.promptLabel = nls.localize('terminal.integrated.a11yPromptLabel', 'Terminal input'); Terminal.strings.tooMuchOutput = nls.localize('terminal.integrated.a11yTooMuchOutput', 'Too much output to announce, navigate to rows manually to read'); } - const accessibilitySupport = this._configurationService.getValue('editor').accessibilitySupport; const font = this._configHelper.getFont(undefined, true); const config = this._configHelper.config; this._xterm = new Terminal({ @@ -413,7 +412,7 @@ export class TerminalInstance implements ITerminalInstance { letterSpacing: font.letterSpacing, lineHeight: font.lineHeight, bellStyle: config.enableBell ? 'sound' : 'none', - screenReaderMode: accessibilitySupport === 'on', + screenReaderMode: this._isScreenReaderOptimized(), macOptionIsMeta: config.macOptionIsMeta, macOptionClickForcesSelection: config.macOptionClickForcesSelection, rightClickSelectsWord: config.rightClickBehavior === 'selectWord', @@ -435,8 +434,8 @@ export class TerminalInstance implements ITerminalInstance { this._xterm.on('data', data => this._processManager!.write(data)); // TODO: How does the cwd work on detached processes? this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform); - this.processReady.then(() => { - this._linkHandler.processCwd = this._processManager!.initialCwd; + this.processReady.then(async () => { + this._linkHandler.processCwd = await this._processManager!.getInitialCwd(); }); } this._xterm.on('focus', () => this._onFocus.fire(this)); @@ -450,6 +449,12 @@ export class TerminalInstance implements ITerminalInstance { this._disposables.push(this._themeService.onThemeChange(theme => this._updateTheme(theme))); } + private _isScreenReaderOptimized(): boolean { + const detected = browser.getAccessibilitySupport() === platform.AccessibilitySupport.Enabled; + const config = this._configurationService.getValue('editor.accessibilitySupport'); + return config === 'on' || (config === 'auto' && detected); + } + public reattachToElement(container: HTMLElement): void { if (!this._wrapperElement) { throw new Error('The terminal instance has not been attached to a container yet'); @@ -575,6 +580,10 @@ export class TerminalInstance implements ITerminalInstance { if (this._processManager) { this._widgetManager = new TerminalWidgetManager(this._wrapperElement); + // HACK: This can be removed once this is fixed upstream xtermjs/xterm.js#1908 + this._disposables.push(dom.addDisposableListener(this._xterm.element, 'mouseleave', () => { + this._widgetManager.closeMessage(); + })); this._linkHandler.setWidgetManager(this._widgetManager); } @@ -1151,8 +1160,7 @@ export class TerminalInstance implements ITerminalInstance { } public updateAccessibilitySupport(): void { - const value = this._configurationService.getValue('editor.accessibilitySupport'); - this._xterm.setOption('screenReaderMode', value === 'on'); + this._xterm.setOption('screenReaderMode', this._isScreenReaderOptimized()); } private _setCursorBlink(blink: boolean): void { @@ -1347,28 +1355,18 @@ export class TerminalInstance implements ITerminalInstance { this._xterm.setOption('debug', this._xterm._core.debug); } - public get initialCwd(): string { - if (this._processManager) { - return this._processManager.initialCwd; + public getInitialCwd(): Promise { + if (!this._processManager) { + return Promise.resolve(''); } - return ''; + return this._processManager.getInitialCwd(); } public getCwd(): Promise { - if (!platform.isWindows) { - let pid = this.processId; - return new Promise(resolve => { - exec('lsof -p ' + pid + ' | grep cwd', (error, stdout, stderr) => { - if (stdout !== '') { - resolve(stdout.substring(stdout.indexOf('/'), stdout.length - 1)); - } - }); - }); - } else { - return new Promise(resolve => { - resolve(this.initialCwd); - }); + if (!this._processManager) { + return Promise.resolve(''); } + return this._processManager.getCwd(); } } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts index 3d726e0cb7a..6395712ee95 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts @@ -151,7 +151,10 @@ export class TerminalPanel extends Panel { this._instantiationService.createInstance(TerminalPasteAction, TerminalPasteAction.ID, TerminalPasteAction.SHORT_LABEL), this._instantiationService.createInstance(SelectAllTerminalAction, SelectAllTerminalAction.ID, SelectAllTerminalAction.LABEL), new Separator(), - this._instantiationService.createInstance(ClearTerminalAction, ClearTerminalAction.ID, ClearTerminalAction.LABEL) + this._instantiationService.createInstance(ClearTerminalAction, ClearTerminalAction.ID, ClearTerminalAction.LABEL), + new Separator(), + this._instantiationService.createInstance(KillTerminalAction, KillTerminalAction.ID, KillTerminalAction.PANEL_LABEL) + ]; this._contextMenuActions.forEach(a => { this._register(a); diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts index ffab899d101..a49259ddcd4 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts @@ -19,6 +19,7 @@ import { IConfigurationResolverService } from 'vs/workbench/services/configurati import { IWindowService } from 'vs/platform/windows/common/windows'; import { Schemas } from 'vs/base/common/network'; import { REMOTE_HOST_SCHEME, getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; +import { sanitizeProcessEnvironment } from 'vs/base/node/processes'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -35,7 +36,6 @@ export class TerminalProcessManager implements ITerminalProcessManager { public processState: ProcessState = ProcessState.UNINITIALIZED; public ptyProcessReady: Promise; public shellProcessId: number; - public initialCwd: string; private _process: ITerminalChildProcess | null = null; private _preLaunchInputQueue: string[] = []; @@ -90,8 +90,8 @@ export class TerminalProcessManager implements ITerminalProcessManager { cols: number, rows: number ): void { - let launchRemotely = false; + const forceExtHostProcess = (this._configHelper.config as any).extHostProcess; if (shellLaunchConfig.cwd && typeof shellLaunchConfig.cwd === 'object') { launchRemotely = !!getRemoteAuthority(shellLaunchConfig.cwd); @@ -100,16 +100,16 @@ export class TerminalProcessManager implements ITerminalProcessManager { launchRemotely = !!this._windowService.getConfiguration().remoteAuthority; } - if (launchRemotely) { - const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(REMOTE_HOST_SCHEME); + if (launchRemotely || forceExtHostProcess) { + const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(forceExtHostProcess ? undefined : REMOTE_HOST_SCHEME); this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, activeWorkspaceRootUri, cols, rows); } else { if (!shellLaunchConfig.executable) { this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig); } - // TODO: @daniel + const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file); - this.initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, activeWorkspaceRootUri, this._configHelper.config.cwd); + const initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, activeWorkspaceRootUri, this._configHelper.config.cwd); // Compel type system as process.env should not have any undefined entries let env: platform.IProcessEnvironment = {}; @@ -134,14 +134,14 @@ export class TerminalProcessManager implements ITerminalProcessManager { // Sanitize the environment, removing any undesirable VS Code and Electron environment // variables - terminalEnvironment.sanitizeEnvironment(env); + sanitizeProcessEnvironment(env); // Adding other env keys necessary to create the process terminalEnvironment.addTerminalEnvironmentKeys(env, platform.locale, this._configHelper.config.setLocaleVariables); } - this._logService.debug(`Terminal process launching`, shellLaunchConfig, this.initialCwd, cols, rows, env); - this._process = new TerminalProcess(shellLaunchConfig, this.initialCwd, cols, rows, env, this._configHelper.config.windowsEnableConpty); + this._logService.debug(`Terminal process launching`, shellLaunchConfig, initialCwd, cols, rows, env); + this._process = new TerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, this._configHelper.config.windowsEnableConpty); } this.processState = ProcessState.LAUNCHING; @@ -201,6 +201,20 @@ export class TerminalProcessManager implements ITerminalProcessManager { } } + public getInitialCwd(): Promise { + if (!this._process) { + return Promise.resolve(''); + } + return this._process.getInitialCwd(); + } + + public getCwd(): Promise { + if (!this._process) { + return Promise.resolve(''); + } + return this._process.getCwd(); + } + private _onExit(exitCode: number): void { this._process = null; diff --git a/src/vs/workbench/parts/terminal/node/terminal.ts b/src/vs/workbench/parts/terminal/node/terminal.ts index 83768c9025f..73b41c24e5d 100644 --- a/src/vs/workbench/parts/terminal/node/terminal.ts +++ b/src/vs/workbench/parts/terminal/node/terminal.ts @@ -10,7 +10,7 @@ import { readFile, fileExists } from 'vs/base/node/pfs'; import { Event } from 'vs/base/common/event'; /** - * An interface representing a raw terminal child process, this is a subset of the + * An interface representing a raw terminal child process, this contains a subset of the * child_process.ChildProcess node.js interface. */ export interface ITerminalChildProcess { @@ -28,6 +28,9 @@ export interface ITerminalChildProcess { shutdown(immediate: boolean): void; input(data: string): void; resize(cols: number, rows: number): void; + + getInitialCwd(): Promise; + getCwd(): Promise; } export function getDefaultShell(p: platform.Platform): string { diff --git a/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts b/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts index 2175f1beb12..7942c4c8700 100644 --- a/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts +++ b/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts @@ -51,24 +51,6 @@ function _mergeEnvironmentValue(env: ITerminalEnvironment, key: string, value: s } } -export function sanitizeEnvironment(env: ITerminalEnvironment): void { - // Remove keys based on strings - const keysToRemove = [ - /^ELECTRON_.+$/, - /^GOOGLE_API_KEY$/, - /^VSCODE_.+$/ - ]; - const envKeys = Object.keys(env); - envKeys.forEach(envKey => { - for (let i = 0; i < keysToRemove.length; i++) { - if (envKey.search(keysToRemove[i]) !== -1) { - delete env[envKey]; - break; - } - } - }); -} - export function addTerminalEnvironmentKeys(env: ITerminalEnvironment, locale: string | undefined, setLocaleVariables: boolean): void { env['TERM_PROGRAM'] = 'vscode'; env['TERM_PROGRAM_VERSION'] = pkg.version; diff --git a/src/vs/workbench/parts/terminal/node/terminalProcess.ts b/src/vs/workbench/parts/terminal/node/terminalProcess.ts index ac913edc443..16dfd0580b4 100644 --- a/src/vs/workbench/parts/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/parts/terminal/node/terminalProcess.ts @@ -11,6 +11,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { ITerminalChildProcess } from 'vs/workbench/parts/terminal/node/terminal'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal'; +import { exec } from 'child_process'; export class TerminalProcess implements ITerminalChildProcess, IDisposable { private _exitCode: number; @@ -20,6 +21,7 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable { private _processStartupComplete: Promise; private _isDisposed: boolean = false; private _titleInterval: NodeJS.Timer | null = null; + private _initialCwd: string; private readonly _onProcessData = new Emitter(); public get onProcessData(): Event { return this._onProcessData.event; } @@ -47,6 +49,7 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable { shellName = 'xterm-256color'; } + this._initialCwd = cwd; const useConpty = windowsEnableConpty && process.platform === 'win32' && this._getWindowsBuildNumber() >= 18309; const options: pty.IPtyForkOptions = { name: shellName, @@ -187,4 +190,24 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable { // exception in winpty. this._ptyProcess.resize(Math.max(cols, 1), Math.max(rows, 1)); } + + public getInitialCwd(): Promise { + return Promise.resolve(this._initialCwd); + } + + public getCwd(): Promise { + if (platform.isWindows) { + return new Promise(resolve => { + resolve(this._initialCwd); + }); + } + + return new Promise(resolve => { + exec('lsof -p ' + this._ptyProcess.pid + ' | grep cwd', (error, stdout, stderr) => { + if (stdout !== '') { + resolve(stdout.substring(stdout.indexOf('/'), stdout.length - 1)); + } + }); + }); + } } diff --git a/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts b/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts index ea06eac26f5..dbf10b10214 100644 --- a/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts @@ -28,6 +28,13 @@ export class TerminalProcessExtHostProxy implements ITerminalChildProcess, ITerm public get onResize(): Event<{ cols: number, rows: number }> { return this._onResize.event; } private readonly _onShutdown = new Emitter(); public get onShutdown(): Event { return this._onShutdown.event; } + private readonly _onRequestInitialCwd = new Emitter(); + public get onRequestInitialCwd(): Event { return this._onRequestInitialCwd.event; } + private readonly _onRequestCwd = new Emitter(); + public get onRequestCwd(): Event { return this._onRequestCwd.event; } + + private _pendingInitialCwdRequests: ((value?: string | Thenable) => void)[] = []; + private _pendingCwdRequests: ((value?: string | Thenable) => void)[] = []; constructor( public terminalId: number, @@ -68,6 +75,18 @@ export class TerminalProcessExtHostProxy implements ITerminalChildProcess, ITerm this.dispose(); } + public emitInitialCwd(initialCwd: string): void { + while (this._pendingInitialCwdRequests.length > 0) { + this._pendingInitialCwdRequests.pop()!(initialCwd); + } + } + + public emitCwd(cwd: string): void { + while (this._pendingCwdRequests.length > 0) { + this._pendingCwdRequests.pop()!(cwd); + } + } + public shutdown(immediate: boolean): void { this._onShutdown.fire(immediate); } @@ -79,4 +98,18 @@ export class TerminalProcessExtHostProxy implements ITerminalChildProcess, ITerm public resize(cols: number, rows: number): void { this._onResize.fire({ cols, rows }); } + + public getInitialCwd(): Promise { + return new Promise(resolve => { + this._onRequestInitialCwd.fire(); + this._pendingInitialCwdRequests.push(resolve); + }); + } + + public getCwd(): Promise { + return new Promise(resolve => { + this._onRequestCwd.fire(); + this._pendingCwdRequests.push(resolve); + }); + } } \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts b/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts index 44806ac20ba..db8533967d8 100644 --- a/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts @@ -32,30 +32,6 @@ suite('Workbench - TerminalEnvironment', () => { assert.equal(env4['LANG'], 'en_US.UTF-8', 'LANG is equal to the parent environment\'s LANG'); }); - test('sanitizeEnvironment', () => { - let env = { - FOO: 'bar', - ELECTRON_ENABLE_STACK_DUMPING: 'x', - ELECTRON_ENABLE_LOGGING: 'x', - 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', - VSCODE_LOGS: 'x', - VSCODE_NLS_CONFIG: 'x', - VSCODE_PORTABLE: 'x', - VSCODE_PID: 'x', - VSCODE_NODE_CACHED_DATA_DIR: 'x', - VSCODE_NEW_VAR: 'x' - }; - terminalEnvironment.sanitizeEnvironment(env); - assert.equal(env['FOO'], 'bar'); - assert.equal(Object.keys(env).length, 1); - }); - suite('mergeEnvironments', () => { test('should add keys', () => { const parent = { diff --git a/src/vs/workbench/parts/watermark/electron-browser/watermark.css b/src/vs/workbench/parts/watermark/electron-browser/watermark.css index 8284b8442dd..15c57d04c3d 100644 --- a/src/vs/workbench/parts/watermark/electron-browser/watermark.css +++ b/src/vs/workbench/parts/watermark/electron-browser/watermark.css @@ -11,11 +11,11 @@ background-position-y: 50%; } -.monaco-workbench > .part.editor > .content > .watermark { +.monaco-workbench .part.editor > .content > .watermark { display: none; /* only visible when no editors are opened */ } -.monaco-workbench > .part.editor.has-watermark > .content.empty > .watermark { +.monaco-workbench .part.editor.has-watermark > .content.empty > .watermark { display: block; position: absolute; width: 100%; @@ -25,50 +25,50 @@ overflow: hidden; } -.monaco-workbench > .part.editor > .content.empty > .watermark > .watermark-box { +.monaco-workbench .part.editor > .content.empty > .watermark > .watermark-box { display: inline-table; border-collapse: separate; border-spacing: 13px 17px; } -.monaco-workbench > .part.editor.max-height-478px > .content.empty > .watermark { +.monaco-workbench .part.editor.max-height-478px > .content.empty > .watermark { display: none; } -.monaco-workbench > .part.editor > .content.empty > .watermark dl { +.monaco-workbench .part.editor > .content.empty > .watermark dl { display: table-row; opacity: .8; cursor: default; } -.monaco-workbench > .part.editor > .content.empty > .watermark dt { +.monaco-workbench .part.editor > .content.empty > .watermark dt { text-align: right; letter-spacing: 0.04em } -.monaco-workbench > .part.editor > .content.empty > .watermark dd { +.monaco-workbench .part.editor > .content.empty > .watermark dd { text-align: left; } -.monaco-workbench > .part.editor > .content.empty > .watermark dt, -.monaco-workbench > .part.editor > .content.empty > .watermark dd { +.monaco-workbench .part.editor > .content.empty > .watermark dt, +.monaco-workbench .part.editor > .content.empty > .watermark dd { display: table-cell; } -.monaco-workbench > .part.editor > .content.empty > .watermark dt, -.monaco-workbench > .part.editor > .content.empty > .watermark dl { +.monaco-workbench .part.editor > .content.empty > .watermark dt, +.monaco-workbench .part.editor > .content.empty > .watermark dl { color: rgba(0,0,0,.68); } -.vs-dark .monaco-workbench > .part.editor > .content.empty > .watermark dt, -.vs-dark .monaco-workbench > .part.editor > .content.empty > .watermark dl { +.vs-dark .monaco-workbench .part.editor > .content.empty > .watermark dt, +.vs-dark .monaco-workbench .part.editor > .content.empty > .watermark dl { color: rgba(255,255,255,.6); } -.hc-black .monaco-workbench > .part.editor > .content.empty > .watermark dt { +.hc-black .monaco-workbench .part.editor > .content.empty > .watermark dt { color: #FFF; } -.hc-black .monaco-workbench > .part.editor > .content.empty > .watermark dl { +.hc-black .monaco-workbench .part.editor > .content.empty > .watermark dl { color: #FFF; opacity: 1; } diff --git a/src/vs/workbench/parts/watermark/electron-browser/watermark.ts b/src/vs/workbench/parts/watermark/electron-browser/watermark.ts index 84b5334c1b5..07b562b3156 100644 --- a/src/vs/workbench/parts/watermark/electron-browser/watermark.ts +++ b/src/vs/workbench/parts/watermark/electron-browser/watermark.ts @@ -15,7 +15,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { OpenRecentAction } from 'vs/workbench/electron-browser/actions'; +import { OpenRecentAction } from 'vs/workbench/electron-browser/actions/windowActions'; import { GlobalNewUntitledFileAction } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { OpenFolderAction, OpenFileFolderAction, OpenFileAction } from 'vs/workbench/browser/actions/workspaceActions'; import { ShowAllCommandsAction } from 'vs/workbench/parts/quickopen/browser/commandsHandler'; diff --git a/src/vs/workbench/parts/webview/electron-browser/baseWebviewEditor.ts b/src/vs/workbench/parts/webview/electron-browser/baseWebviewEditor.ts index 7d3a3a77fbc..0851ec06807 100644 --- a/src/vs/workbench/parts/webview/electron-browser/baseWebviewEditor.ts +++ b/src/vs/workbench/parts/webview/electron-browser/baseWebviewEditor.ts @@ -55,26 +55,44 @@ export abstract class BaseWebviewEditor extends BaseEditor { } public reload() { - if (this._webview) { - this._webview.reload(); - } + this.withWebviewElement(webview => webview.reload()); } public layout(dimension: Dimension): void { - if (this._webview) { - this._webview.layout(); - } + this.withWebviewElement(webview => webview.layout()); } public focus(): void { - if (this._webview) { - this._webview.focus(); - } + this.withWebviewElement(webview => webview.focus()); } public selectAll(): void { + this.withWebviewElement(webview => webview.selectAll()); + } + + public copy(): void { + this.withWebviewElement(webview => webview.copy()); + } + + public paste(): void { + this.withWebviewElement(webview => webview.paste()); + } + + public cut(): void { + this.withWebviewElement(webview => webview.cut()); + } + + public undo(): void { + this.withWebviewElement(webview => webview.undo()); + } + + public redo(): void { + this.withWebviewElement(webview => webview.redo()); + } + + private withWebviewElement(f: (element: WebviewElement) => void): void { if (this._webview) { - this._webview.selectAll(); + f(this._webview); } } } diff --git a/src/vs/workbench/parts/webview/electron-browser/webview.contribution.ts b/src/vs/workbench/parts/webview/electron-browser/webview.contribution.ts index 8adfb937562..033bd98d007 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webview.contribution.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webview.contribution.ts @@ -16,11 +16,12 @@ import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/wor import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import { WebviewEditorInputFactory } from 'vs/workbench/parts/webview/electron-browser/webviewEditorInputFactory'; import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from './baseWebviewEditor'; -import { HideWebViewEditorFindCommand, OpenWebviewDeveloperToolsAction, ReloadWebviewAction, ShowWebViewEditorFindWidgetCommand, SelectAllWebviewEditorCommand } from './webviewCommands'; +import { HideWebViewEditorFindCommand, OpenWebviewDeveloperToolsAction, ReloadWebviewAction, ShowWebViewEditorFindWidgetCommand, SelectAllWebviewEditorCommand, CopyWebviewEditorCommand, PasteWebviewEditorCommand, CutWebviewEditorCommand, UndoWebviewEditorCommand, RedoWebviewEditorCommand } from './webviewCommands'; import { WebviewEditor } from './webviewEditor'; import { WebviewEditorInput } from './webviewEditorInput'; import { IWebviewEditorService, WebviewEditorService } from './webviewEditorService'; import { InputFocusedContextKey } from 'vs/platform/workbench/common/contextkeys'; +import { isMacintosh } from 'vs/base/common/platform'; (Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor( WebviewEditor, @@ -52,7 +53,7 @@ export function registerWebViewCommands(editorId: string): void { }); showNextFindWidgetCommand.register(); - const hideCommand = new HideWebViewEditorFindCommand({ + (new HideWebViewEditorFindCommand({ id: HideWebViewEditorFindCommand.ID, precondition: ContextKeyExpr.and( contextKeyExpr, @@ -61,18 +62,67 @@ export function registerWebViewCommands(editorId: string): void { primary: KeyCode.Escape, weight: KeybindingWeight.EditorContrib } - }); - hideCommand.register(); + })).register(); - const selectAllCommand = new SelectAllWebviewEditorCommand({ + (new SelectAllWebviewEditorCommand({ id: SelectAllWebviewEditorCommand.ID, precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), kbOpts: { primary: KeyMod.CtrlCmd | KeyCode.KEY_A, weight: KeybindingWeight.EditorContrib } - }); - selectAllCommand.register(); + })).register(); + + // These commands are only needed on MacOS where we have to disable the menu bar commands + if (isMacintosh) { + (new CopyWebviewEditorCommand({ + id: CopyWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_C, + weight: KeybindingWeight.EditorContrib + } + })).register(); + + (new PasteWebviewEditorCommand({ + id: PasteWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_V, + weight: KeybindingWeight.EditorContrib + } + })).register(); + + + (new CutWebviewEditorCommand({ + id: CutWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_X, + weight: KeybindingWeight.EditorContrib + } + })).register(); + + (new UndoWebviewEditorCommand({ + id: UndoWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_Z, + weight: KeybindingWeight.EditorContrib + } + })).register(); + + (new RedoWebviewEditorCommand({ + id: RedoWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_Y, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z], + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z }, + weight: KeybindingWeight.EditorContrib + } + })).register(); + } } registerWebViewCommands(WebviewEditor.ID); diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewCommands.ts b/src/vs/workbench/parts/webview/electron-browser/webviewCommands.ts index 4f42f3bf403..b91a3fa1224 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewCommands.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewCommands.ts @@ -43,6 +43,61 @@ export class SelectAllWebviewEditorCommand extends Command { } } +export class CopyWebviewEditorCommand extends Command { + public static readonly ID = 'editor.action.webvieweditor.copy'; + + public runCommand(accessor: ServicesAccessor, args: any): void { + const webViewEditor = getActiveWebviewEditor(accessor); + if (webViewEditor) { + webViewEditor.copy(); + } + } +} + +export class PasteWebviewEditorCommand extends Command { + public static readonly ID = 'editor.action.webvieweditor.paste'; + + public runCommand(accessor: ServicesAccessor, args: any): void { + const webViewEditor = getActiveWebviewEditor(accessor); + if (webViewEditor) { + webViewEditor.paste(); + } + } +} + +export class CutWebviewEditorCommand extends Command { + public static readonly ID = 'editor.action.webvieweditor.cut'; + + public runCommand(accessor: ServicesAccessor, args: any): void { + const webViewEditor = getActiveWebviewEditor(accessor); + if (webViewEditor) { + webViewEditor.cut(); + } + } +} + +export class UndoWebviewEditorCommand extends Command { + public static readonly ID = 'editor.action.webvieweditor.undo'; + + public runCommand(accessor: ServicesAccessor, args: any): void { + const webViewEditor = getActiveWebviewEditor(accessor); + if (webViewEditor) { + webViewEditor.undo(); + } + } +} + +export class RedoWebviewEditorCommand extends Command { + public static readonly ID = 'editor.action.webvieweditor.redo'; + + public runCommand(accessor: ServicesAccessor, args: any): void { + const webViewEditor = getActiveWebviewEditor(accessor); + if (webViewEditor) { + webViewEditor.redo(); + } + } +} + export class OpenWebviewDeveloperToolsAction extends Action { static readonly ID = 'workbench.action.webview.openDeveloperTools'; static readonly LABEL = nls.localize('openToolsLabel', "Open Webview Developer Tools"); diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewElement.ts b/src/vs/workbench/parts/webview/electron-browser/webviewElement.ts index b6a9ea29e72..5d9110300fb 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewElement.ts @@ -157,7 +157,9 @@ class WebviewKeyboardHandler extends Disposable { const contents = this.getWebContents(); if (contents) { contents.on('before-input-event', (_event, input) => { - this.setIgnoreMenuShortcuts(input.control || input.meta); + if (input.type === 'keyDown') { + this.setIgnoreMenuShortcuts(input.control || input.meta); + } }); } })); @@ -574,6 +576,26 @@ export class WebviewElement extends Disposable { public selectAll() { this._webview.selectAll(); } + + public copy() { + this._webview.copy(); + } + + public paste() { + this._webview.paste(); + } + + public cut() { + this._webview.cut(); + } + + public undo() { + this._webview.undo(); + } + + public redo() { + this._webview.redo(); + } } diff --git a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.css b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.css index 94d52d379fd..75a18c03da3 100644 --- a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.css +++ b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench > .part.editor > .content .welcomePageContainer { +.monaco-workbench .part.editor > .content .welcomePageContainer { align-items: center; display: flex; justify-content: center; @@ -11,51 +11,51 @@ min-height: 100%; } -.monaco-workbench > .part.editor > .content .welcomePage { +.monaco-workbench .part.editor > .content .welcomePage { width: 90%; max-width: 1200px; font-size: 10px; } -.monaco-workbench > .part.editor > .content .welcomePage .row { +.monaco-workbench .part.editor > .content .welcomePage .row { display: flex; flex-flow: row; } -.monaco-workbench > .part.editor > .content .welcomePage .row .section { +.monaco-workbench .part.editor > .content .welcomePage .row .section { overflow: hidden; } -.monaco-workbench > .part.editor > .content .welcomePage .row .splash { +.monaco-workbench .part.editor > .content .welcomePage .row .splash { overflow: hidden; } -.monaco-workbench > .part.editor > .content .welcomePage .row .commands { +.monaco-workbench .part.editor > .content .welcomePage .row .commands { overflow: hidden; } -.monaco-workbench > .part.editor > .content .welcomePage .row .commands .list { +.monaco-workbench .part.editor > .content .welcomePage .row .commands .list { overflow: hidden; } -.monaco-workbench > .part.editor > .content .welcomePage p { +.monaco-workbench .part.editor > .content .welcomePage p { font-size: 1.3em; } -.monaco-workbench > .part.editor > .content .welcomePage .keyboard { +.monaco-workbench .part.editor > .content .welcomePage .keyboard { font-family: "Lucida Grande", sans-serif;/* Keyboard shortcuts */ } -.monaco-workbench > .part.editor > .content .welcomePage a { +.monaco-workbench .part.editor > .content .welcomePage a { text-decoration: none; } -.monaco-workbench > .part.editor > .content .welcomePage a:focus { +.monaco-workbench .part.editor > .content .welcomePage a:focus { outline: 1px solid -webkit-focus-ring-color; outline-offset: -1px; } -.monaco-workbench > .part.editor > .content .welcomePage h1 { +.monaco-workbench .part.editor > .content .welcomePage h1 { padding: 0; margin: 0; border: none; @@ -64,28 +64,28 @@ white-space: nowrap; } -.monaco-workbench > .part.editor > .content .welcomePage .title { +.monaco-workbench .part.editor > .content .welcomePage .title { margin-top: 1em; margin-bottom: 1em; flex: 1 100%; } -.monaco-workbench > .part.editor > .content .welcomePage .subtitle { +.monaco-workbench .part.editor > .content .welcomePage .subtitle { margin-top: .8em; font-size: 2.6em; display: block; } -.hc-black .monaco-workbench > .part.editor > .content .welcomePage .subtitle { +.hc-black .monaco-workbench .part.editor > .content .welcomePage .subtitle { font-weight: 200; } -.monaco-workbench > .part.editor > .content .welcomePage .splash, -.monaco-workbench > .part.editor > .content .welcomePage .commands { +.monaco-workbench .part.editor > .content .welcomePage .splash, +.monaco-workbench .part.editor > .content .welcomePage .commands { flex: 1 1 0; } -.monaco-workbench > .part.editor > .content .welcomePage h2 { +.monaco-workbench .part.editor > .content .welcomePage h2 { font-weight: 200; margin-top: 17px; margin-bottom: 5px; @@ -93,62 +93,62 @@ line-height: initial; } -.monaco-workbench > .part.editor > .content .welcomePage .splash .section { +.monaco-workbench .part.editor > .content .welcomePage .splash .section { margin-bottom: 5em; } -.monaco-workbench > .part.editor > .content .welcomePage .splash ul { +.monaco-workbench .part.editor > .content .welcomePage .splash ul { margin: 0; font-size: 1.3em; list-style: none; padding: 0; } -.monaco-workbench > .part.editor > .content .welcomePage .splash li { +.monaco-workbench .part.editor > .content .welcomePage .splash li { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.monaco-workbench > .part.editor > .content .welcomePage.emptyRecent .splash .recent .list { +.monaco-workbench .part.editor > .content .welcomePage.emptyRecent .splash .recent .list { display: none; } -.monaco-workbench > .part.editor > .content .welcomePage .splash .recent .none { +.monaco-workbench .part.editor > .content .welcomePage .splash .recent .none { display: none; } -.monaco-workbench > .part.editor > .content .welcomePage.emptyRecent .splash .recent .none { +.monaco-workbench .part.editor > .content .welcomePage.emptyRecent .splash .recent .none { display: initial; } -.monaco-workbench > .part.editor > .content .welcomePage .splash .recent li.moreRecent { +.monaco-workbench .part.editor > .content .welcomePage .splash .recent li.moreRecent { margin-top: 5px; } -.monaco-workbench > .part.editor > .content .welcomePage .splash .recent .path { +.monaco-workbench .part.editor > .content .welcomePage .splash .recent .path { padding-left: 1em; } -.monaco-workbench > .part.editor > .content .welcomePage .splash .title, -.monaco-workbench > .part.editor > .content .welcomePage .splash .showOnStartup { +.monaco-workbench .part.editor > .content .welcomePage .splash .title, +.monaco-workbench .part.editor > .content .welcomePage .splash .showOnStartup { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.monaco-workbench > .part.editor > .content .welcomePage .splash .showOnStartup > .checkbox { +.monaco-workbench .part.editor > .content .welcomePage .splash .showOnStartup > .checkbox { vertical-align: bottom; } -.monaco-workbench > .part.editor > .content .welcomePage .commands .list { +.monaco-workbench .part.editor > .content .welcomePage .commands .list { list-style: none; padding: 0; } -.monaco-workbench > .part.editor > .content .welcomePage .commands .item { +.monaco-workbench .part.editor > .content .welcomePage .commands .item { margin: 7px 0px; } -.monaco-workbench > .part.editor > .content .welcomePage .commands .item button { +.monaco-workbench .part.editor > .content .welcomePage .commands .item button { margin: 1px; padding: 12px 10px; width: calc(100% - 2px); @@ -160,7 +160,7 @@ font-family: inherit; } -.monaco-workbench > .part.editor > .content .welcomePage .commands .item button > span { +.monaco-workbench .part.editor > .content .welcomePage .commands .item button > span { display: inline-block; width:100%; min-width: 0; @@ -169,7 +169,7 @@ text-overflow: ellipsis; } -.monaco-workbench > .part.editor > .content .welcomePage .commands .item button h3 { +.monaco-workbench .part.editor > .content .welcomePage .commands .item button h3 { font-weight: normal; font-size: 1em; margin: 0; @@ -180,41 +180,41 @@ text-overflow: ellipsis; } -.monaco-workbench > .part.editor > .content .welcomePage .commands .item button { +.monaco-workbench .part.editor > .content .welcomePage .commands .item button { border: none; } -.hc-black .monaco-workbench > .part.editor > .content .welcomePage .commands .item button > h3 { +.hc-black .monaco-workbench .part.editor > .content .welcomePage .commands .item button > h3 { font-weight: bold; } -.monaco-workbench > .part.editor > .content .welcomePage .commands .item button:focus { +.monaco-workbench .part.editor > .content .welcomePage .commands .item button:focus { outline-style: solid; outline-width: 1px; } -.hc-black .monaco-workbench > .part.editor > .content .welcomePage .commands .item button { +.hc-black .monaco-workbench .part.editor > .content .welcomePage .commands .item button { border-width: 1px; border-style: solid; } -.hc-black .monaco-workbench > .part.editor > .content .welcomePage .commands .item button:hover { +.hc-black .monaco-workbench .part.editor > .content .welcomePage .commands .item button:hover { outline-width: 1px; outline-style: dashed; outline-offset: -5px; } -.monaco-workbench > .part.editor > .content .welcomePage .commands .item button .enabledExtension { +.monaco-workbench .part.editor > .content .welcomePage .commands .item button .enabledExtension { display: none; } -.monaco-workbench > .part.editor > .content .welcomePage .commands .item button .installExtension.installed { +.monaco-workbench .part.editor > .content .welcomePage .commands .item button .installExtension.installed { display: none; } -.monaco-workbench > .part.editor > .content .welcomePage .commands .item button .enabledExtension.installed { +.monaco-workbench .part.editor > .content .welcomePage .commands .item button .enabledExtension.installed { display: inline; } -.monaco-workbench > .part.editor > .content .welcomePageContainer.max-height-685px .title { +.monaco-workbench .part.editor > .content .welcomePageContainer.max-height-685px .title { display: none; } @@ -223,9 +223,9 @@ background-image: url('../../code-icon.svg'); } -.monaco-workbench > .part.editor > .content .welcomePage .mac-only, -.monaco-workbench > .part.editor > .content .welcomePage .windows-only, -.monaco-workbench > .part.editor > .content .welcomePage .linux-only { +.monaco-workbench .part.editor > .content .welcomePage .mac-only, +.monaco-workbench .part.editor > .content .welcomePage .windows-only, +.monaco-workbench .part.editor > .content .welcomePage .linux-only { display: none; } .mac > .monaco-workbench > .part.editor > .content .welcomePage .mac-only { diff --git a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts index 0a127775cae..3e7bb14b1af 100644 --- a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts +++ b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts @@ -640,43 +640,43 @@ export const welcomePageBackground = registerColor('welcomePage.background', { l registerThemingParticipant((theme, collector) => { const backgroundColor = theme.getColor(welcomePageBackground); if (backgroundColor) { - collector.addRule(`.monaco-workbench > .part.editor > .content .welcomePageContainer { background-color: ${backgroundColor}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .welcomePageContainer { background-color: ${backgroundColor}; }`); } const foregroundColor = theme.getColor(foreground); if (foregroundColor) { - collector.addRule(`.monaco-workbench > .part.editor > .content .welcomePage .caption { color: ${foregroundColor}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .welcomePage .caption { color: ${foregroundColor}; }`); } const descriptionColor = theme.getColor(descriptionForeground); if (descriptionColor) { - collector.addRule(`.monaco-workbench > .part.editor > .content .welcomePage .detail { color: ${descriptionColor}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .welcomePage .detail { color: ${descriptionColor}; }`); } const buttonColor = getExtraColor(theme, buttonBackground, { dark: 'rgba(0, 0, 0, .2)', extra_dark: 'rgba(200, 235, 255, .042)', light: 'rgba(0,0,0,.04)', hc: 'black' }); if (buttonColor) { - collector.addRule(`.monaco-workbench > .part.editor > .content .welcomePage .commands .item button { background: ${buttonColor}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .welcomePage .commands .item button { background: ${buttonColor}; }`); } const buttonHoverColor = getExtraColor(theme, buttonHoverBackground, { dark: 'rgba(200, 235, 255, .072)', extra_dark: 'rgba(200, 235, 255, .072)', light: 'rgba(0,0,0,.10)', hc: null }); if (buttonHoverColor) { - collector.addRule(`.monaco-workbench > .part.editor > .content .welcomePage .commands .item button:hover { background: ${buttonHoverColor}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .welcomePage .commands .item button:hover { background: ${buttonHoverColor}; }`); } const link = theme.getColor(textLinkForeground); if (link) { - collector.addRule(`.monaco-workbench > .part.editor > .content .welcomePage a { color: ${link}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .welcomePage a { color: ${link}; }`); } const activeLink = theme.getColor(textLinkActiveForeground); if (activeLink) { - collector.addRule(`.monaco-workbench > .part.editor > .content .welcomePage a:hover, - .monaco-workbench > .part.editor > .content .welcomePage a:active { color: ${activeLink}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .welcomePage a:hover, + .monaco-workbench .part.editor > .content .welcomePage a:active { color: ${activeLink}; }`); } const focusColor = theme.getColor(focusBorder); if (focusColor) { - collector.addRule(`.monaco-workbench > .part.editor > .content .welcomePage a:focus { outline-color: ${focusColor}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .welcomePage a:focus { outline-color: ${focusColor}; }`); } const border = theme.getColor(contrastBorder); if (border) { - collector.addRule(`.monaco-workbench > .part.editor > .content .welcomePage .commands .item button { border-color: ${border}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .welcomePage .commands .item button { border-color: ${border}; }`); } const activeBorder = theme.getColor(activeContrastBorder); if (activeBorder) { - collector.addRule(`.monaco-workbench > .part.editor > .content .welcomePage .commands .item button:hover { outline-color: ${activeBorder}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .welcomePage .commands .item button:hover { outline-color: ${activeBorder}; }`); } }); diff --git a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.css b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.css index 56a76aa42fe..c83ac377cae 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.css +++ b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.css @@ -3,45 +3,45 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench > .part.editor > .content .walkThroughContent { +.monaco-workbench .part.editor > .content .walkThroughContent { box-sizing: border-box; padding: 10px 20px; line-height: 22px; user-select: initial; } -.monaco-workbench > .part.editor > .content .walkThroughContent img { +.monaco-workbench .part.editor > .content .walkThroughContent img { max-width: 100%; max-height: 100%; } -.monaco-workbench > .part.editor > .content .walkThroughContent a { +.monaco-workbench .part.editor > .content .walkThroughContent a { text-decoration: none; } -.monaco-workbench > .part.editor > .content .walkThroughContent a:focus, -.monaco-workbench > .part.editor > .content .walkThroughContent input:focus, -.monaco-workbench > .part.editor > .content .walkThroughContent select:focus, -.monaco-workbench > .part.editor > .content .walkThroughContent textarea:focus { +.monaco-workbench .part.editor > .content .walkThroughContent a:focus, +.monaco-workbench .part.editor > .content .walkThroughContent input:focus, +.monaco-workbench .part.editor > .content .walkThroughContent select:focus, +.monaco-workbench .part.editor > .content .walkThroughContent textarea:focus { outline: 1px solid -webkit-focus-ring-color; outline-offset: -1px; } -.monaco-workbench > .part.editor > .content .walkThroughContent hr { +.monaco-workbench .part.editor > .content .walkThroughContent hr { border: 0; height: 2px; border-bottom: 2px solid; } -.monaco-workbench > .part.editor > .content .walkThroughContent h1, -.monaco-workbench > .part.editor > .content .walkThroughContent h2, -.monaco-workbench > .part.editor > .content .walkThroughContent h3 { +.monaco-workbench .part.editor > .content .walkThroughContent h1, +.monaco-workbench .part.editor > .content .walkThroughContent h2, +.monaco-workbench .part.editor > .content .walkThroughContent h3 { font-weight: lighter; margin-top: 20px; margin-bottom: 10px; } -.monaco-workbench > .part.editor > .content .walkThroughContent h1 { +.monaco-workbench .part.editor > .content .walkThroughContent h1 { padding-bottom: 0.3em; line-height: 1.2; border-bottom-width: 1px; @@ -50,66 +50,66 @@ margin-bottom: 15px; } -.monaco-workbench > .part.editor > .content .walkThroughContent h2 { +.monaco-workbench .part.editor > .content .walkThroughContent h2 { font-size: 30px; margin-top: 30px; } -.monaco-workbench > .part.editor > .content .walkThroughContent h3 { +.monaco-workbench .part.editor > .content .walkThroughContent h3 { font-size: 22px; } -.monaco-workbench > .part.editor > .content .walkThroughContent h4 { +.monaco-workbench .part.editor > .content .walkThroughContent h4 { font-size: 12px; text-transform: uppercase; margin-top: 30px; margin-bottom: 10px; } -.monaco-workbench > .part.editor > .content .walkThroughContent a:hover { +.monaco-workbench .part.editor > .content .walkThroughContent a:hover { text-decoration: underline; } -.monaco-workbench > .part.editor > .content .walkThroughContent table { +.monaco-workbench .part.editor > .content .walkThroughContent table { border-collapse: collapse; } -.monaco-workbench > .part.editor > .content .walkThroughContent table > thead > tr > th { +.monaco-workbench .part.editor > .content .walkThroughContent table > thead > tr > th { text-align: left; border-bottom: 1px solid; } -.monaco-workbench > .part.editor > .content .walkThroughContent table > thead > tr > th, -.monaco-workbench > .part.editor > .content .walkThroughContent table > thead > tr > td, -.monaco-workbench > .part.editor > .content .walkThroughContent table > tbody > tr > th, -.monaco-workbench > .part.editor > .content .walkThroughContent table > tbody > tr > td { +.monaco-workbench .part.editor > .content .walkThroughContent table > thead > tr > th, +.monaco-workbench .part.editor > .content .walkThroughContent table > thead > tr > td, +.monaco-workbench .part.editor > .content .walkThroughContent table > tbody > tr > th, +.monaco-workbench .part.editor > .content .walkThroughContent table > tbody > tr > td { padding: 5px 10px; } -.monaco-workbench > .part.editor > .content .walkThroughContent table > tbody > tr + tr > td { +.monaco-workbench .part.editor > .content .walkThroughContent table > tbody > tr + tr > td { border-top: 1px solid; } -.monaco-workbench > .part.editor > .content .walkThroughContent blockquote { +.monaco-workbench .part.editor > .content .walkThroughContent blockquote { margin: 0 7px 0 5px; padding: 0 16px 0 10px; border-left: 5px solid; } -.monaco-workbench > .part.editor > .content .walkThroughContent code, -.monaco-workbench > .part.editor > .content .walkThroughContent .shortcut { +.monaco-workbench .part.editor > .content .walkThroughContent code, +.monaco-workbench .part.editor > .content .walkThroughContent .shortcut { font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; font-size: 14px; line-height: 19px; } -.monaco-workbench > .part.editor > .content .walkThroughContent blockquote { +.monaco-workbench .part.editor > .content .walkThroughContent blockquote { margin: 0 7px 0 5px; padding: 0 16px 0 10px; border-left: 5px solid; } -.monaco-workbench > .part.editor > .content .walkThroughContent .monaco-tokenized-source { +.monaco-workbench .part.editor > .content .walkThroughContent .monaco-tokenized-source { white-space: pre; } @@ -118,9 +118,9 @@ background-image: url('../../code-icon.svg'); } -.monaco-workbench > .part.editor > .content .walkThroughContent .mac-only, -.monaco-workbench > .part.editor > .content .walkThroughContent .windows-only, -.monaco-workbench > .part.editor > .content .walkThroughContent .linux-only { +.monaco-workbench .part.editor > .content .walkThroughContent .mac-only, +.monaco-workbench .part.editor > .content .walkThroughContent .windows-only, +.monaco-workbench .part.editor > .content .walkThroughContent .linux-only { display: none; } .mac > .monaco-workbench > .part.editor > .content .walkThroughContent .mac-only { @@ -133,7 +133,7 @@ display: initial; } -.hc-black .monaco-workbench > .part.editor > .content .walkThroughContent .monaco-editor { +.hc-black .monaco-workbench .part.editor > .content .walkThroughContent .monaco-editor { border-width: 1px; border-style: solid; } diff --git a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts index 533bd83e758..ed99e06ee17 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts +++ b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts @@ -526,37 +526,37 @@ export const embeddedEditorBackground = registerColor('walkThrough.embeddedEdito registerThemingParticipant((theme, collector) => { const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null }); if (color) { - collector.addRule(`.monaco-workbench > .part.editor > .content .walkThroughContent .monaco-editor-background, - .monaco-workbench > .part.editor > .content .walkThroughContent .margin-view-overlays { background: ${color}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .monaco-editor-background, + .monaco-workbench .part.editor > .content .walkThroughContent .margin-view-overlays { background: ${color}; }`); } const link = theme.getColor(textLinkForeground); if (link) { - collector.addRule(`.monaco-workbench > .part.editor > .content .walkThroughContent a { color: ${link}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent a { color: ${link}; }`); } const activeLink = theme.getColor(textLinkActiveForeground); if (activeLink) { - collector.addRule(`.monaco-workbench > .part.editor > .content .walkThroughContent a:hover, - .monaco-workbench > .part.editor > .content .walkThroughContent a:active { color: ${activeLink}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent a:hover, + .monaco-workbench .part.editor > .content .walkThroughContent a:active { color: ${activeLink}; }`); } const focusColor = theme.getColor(focusBorder); if (focusColor) { - collector.addRule(`.monaco-workbench > .part.editor > .content .walkThroughContent a:focus { outline-color: ${focusColor}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent a:focus { outline-color: ${focusColor}; }`); } const shortcut = theme.getColor(textPreformatForeground); if (shortcut) { - collector.addRule(`.monaco-workbench > .part.editor > .content .walkThroughContent code, - .monaco-workbench > .part.editor > .content .walkThroughContent .shortcut { color: ${shortcut}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent code, + .monaco-workbench .part.editor > .content .walkThroughContent .shortcut { color: ${shortcut}; }`); } const border = theme.getColor(contrastBorder); if (border) { - collector.addRule(`.monaco-workbench > .part.editor > .content .walkThroughContent .monaco-editor { border-color: ${border}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .monaco-editor { border-color: ${border}; }`); } const quoteBackground = theme.getColor(textBlockQuoteBackground); if (quoteBackground) { - collector.addRule(`.monaco-workbench > .part.editor > .content .walkThroughContent blockquote { background: ${quoteBackground}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent blockquote { background: ${quoteBackground}; }`); } const quoteBorder = theme.getColor(textBlockQuoteBorder); if (quoteBorder) { - collector.addRule(`.monaco-workbench > .part.editor > .content .walkThroughContent blockquote { border-color: ${quoteBorder}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent blockquote { border-color: ${quoteBorder}; }`); } }); diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index 1148a763638..4a8acfeda40 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { dirname } from 'path'; import * as assert from 'vs/base/common/assert'; import { Event, Emitter } from 'vs/base/common/event'; import { ResourceMap } from 'vs/base/common/map'; @@ -36,7 +35,7 @@ import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/works import { UserConfiguration } from 'vs/platform/configuration/node/configuration'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { localize } from 'vs/nls'; -import { isEqual } from 'vs/base/common/resources'; +import { isEqual, dirname } from 'vs/base/common/resources'; import { mark } from 'vs/base/common/performance'; export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService { @@ -44,6 +43,8 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat public _serviceBrand: any; private workspace: Workspace; + private resolvePromise: Promise; + private resolveCallback: () => void; private _configuration: Configuration; private defaultConfiguration: DefaultConfigurationModel; private userConfiguration: UserConfiguration; @@ -71,6 +72,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat constructor(private environmentService: IEnvironmentService, private workspaceSettingsRootFolder: string = FOLDER_CONFIG_FOLDER_NAME) { super(); + this.resolvePromise = new Promise(c => this.resolveCallback = c); this.defaultConfiguration = new DefaultConfigurationModel(); this.userConfiguration = this._register(new UserConfiguration(environmentService.appSettingsPath)); this.workspaceConfiguration = this._register(new WorkspaceConfiguration(environmentService)); @@ -85,6 +87,10 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat // Workspace Context Service Impl + public getCompleteWorkspace(): Promise { + return this.resolvePromise.then(() => this.getWorkspace()); + } + public getWorkspace(): Workspace { return this.workspace; } @@ -162,8 +168,8 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat if (foldersToAdd.length) { // Recompute current workspace folders if we have folders to add - const workspaceConfigFolder = dirname(this.getWorkspace().configuration.fsPath); - currentWorkspaceFolders = toWorkspaceFolders(newStoredFolders, URI.file(workspaceConfigFolder)); + const workspaceConfigFolder = dirname(this.getWorkspace().configuration); + currentWorkspaceFolders = toWorkspaceFolders(newStoredFolders, workspaceConfigFolder); const currentWorkspaceFolderUris = currentWorkspaceFolders.map(folder => folder.uri); const storedFoldersToAdd: IStoredWorkspaceFolder[] = []; @@ -318,6 +324,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat for (const workspaceFolder of changedWorkspaceFolders) { this.onWorkspaceFolderConfigurationChanged(workspaceFolder); } + this.resolveCallback(); }); } @@ -342,7 +349,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return this.workspaceConfiguration.load({ id: workspaceIdentifier.id, configPath: URI.file(workspaceIdentifier.configPath) }) .then(() => { const workspaceConfigPath = URI.file(workspaceIdentifier.configPath); - const workspaceFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), URI.file(dirname(workspaceConfigPath.fsPath))); + const workspaceFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), dirname(workspaceConfigPath)); const workspaceId = workspaceIdentifier.id; return new Workspace(workspaceId, workspaceFolders, workspaceConfigPath); }); @@ -535,7 +542,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat private onWorkspaceConfigurationChanged(): Promise { if (this.workspace && this.workspace.configuration && this._configuration) { const workspaceConfigurationChangeEvent = this._configuration.compareAndUpdateWorkspaceConfiguration(this.workspaceConfiguration.getConfiguration()); - let configuredFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), URI.file(dirname(this.workspace.configuration.fsPath))); + let configuredFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), dirname(this.workspace.configuration)); const changes = this.compareFolders(this.workspace.folders, configuredFolders); if (changes.added.length || changes.removed.length || changes.changed.length) { this.workspace.folders = configuredFolders; diff --git a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts index 89bb593da04..f34d75f6120 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts @@ -163,7 +163,8 @@ export class FileDialogService implements IFileDialogService { @IEnvironmentService private readonly environmentService: IEnvironmentService ) { } - public defaultFilePath(schemeFilter: string): URI | undefined { + defaultFilePath(schemeFilter: string): URI | undefined { + // Check for last active file first... let candidate = this.historyService.getLastActiveFile(schemeFilter); @@ -175,7 +176,8 @@ export class FileDialogService implements IFileDialogService { return candidate && resources.dirname(candidate) || undefined; } - public defaultFolderPath(schemeFilter: string): URI | undefined { + defaultFolderPath(schemeFilter: string): URI | undefined { + // Check for last active file root first... let candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter); @@ -187,7 +189,7 @@ export class FileDialogService implements IFileDialogService { return candidate && resources.dirname(candidate) || undefined; } - public defaultWorkspacePath(schemeFilter: string): URI | undefined { + defaultWorkspacePath(schemeFilter: string): URI | undefined { // Check for current workspace config file first... if (schemeFilter === Schemas.file && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { @@ -211,36 +213,39 @@ export class FileDialogService implements IFileDialogService { }; } - public pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise { - let defaultUri = options.defaultUri; + pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise { + const defaultUri = options.defaultUri; if (!defaultUri) { options.defaultUri = this.defaultFilePath(Schemas.file); } - return this.windowService.pickFileFolderAndOpen(this.toNativeOpenDialogOptions(options)); + return this.windowService.pickFileFolderAndOpen(this.toNativeOpenDialogOptions(options)); } - public pickFileAndOpen(options: IPickAndOpenOptions): Promise { - let defaultUri = options.defaultUri; + pickFileAndOpen(options: IPickAndOpenOptions): Promise { + const defaultUri = options.defaultUri; if (!defaultUri) { options.defaultUri = this.defaultFilePath(Schemas.file); } + return this.windowService.pickFileAndOpen(this.toNativeOpenDialogOptions(options)); } - public pickFolderAndOpen(options: IPickAndOpenOptions): Promise { - let defaultUri = options.defaultUri; + pickFolderAndOpen(options: IPickAndOpenOptions): Promise { + const defaultUri = options.defaultUri; if (!defaultUri) { options.defaultUri = this.defaultFolderPath(Schemas.file); } + return this.windowService.pickFolderAndOpen(this.toNativeOpenDialogOptions(options)); } - public pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise { - let defaultUri = options.defaultUri; + pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise { + const defaultUri = options.defaultUri; if (!defaultUri) { options.defaultUri = this.defaultWorkspacePath(Schemas.file); } + return this.windowService.pickWorkspaceAndOpen(this.toNativeOpenDialogOptions(options)); } @@ -253,20 +258,22 @@ export class FileDialogService implements IFileDialogService { }; } - public showSaveDialog(options: ISaveDialogOptions): Promise { + showSaveDialog(options: ISaveDialogOptions): Promise { const defaultUri = options.defaultUri; if (defaultUri && defaultUri.scheme !== Schemas.file) { return Promise.reject(new Error('Not supported - Save-dialogs can only be opened on `file`-uris.')); } + return this.windowService.showSaveDialog(this.toNativeSaveDialogOptions(options)).then(result => { if (result) { return URI.file(result); } + return undefined; }); } - public showOpenDialog(options: IOpenDialogOptions): Promise { + showOpenDialog(options: IOpenDialogOptions): Promise { const defaultUri = options.defaultUri; if (defaultUri && defaultUri.scheme !== Schemas.file) { return Promise.reject(new Error('Not supported - Open-dialogs can only be opened on `file`-uris.')); @@ -279,16 +286,21 @@ export class FileDialogService implements IFileDialogService { filters: options.filters, properties: [] }; + newOptions.properties!.push('createDirectory'); + if (options.canSelectFiles) { newOptions.properties!.push('openFile'); } + if (options.canSelectFolders) { newOptions.properties!.push('openDirectory'); } + if (options.canSelectMany) { newOptions.properties!.push('multiSelections'); } + return this.windowService.showOpenDialog(newOptions).then(result => result ? result.map(URI.file) : undefined); } } diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index 267af62dc3c..45cbfeb9953 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -15,7 +15,6 @@ import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService import { EditorService, DelegatingEditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IEditorGroup, IEditorGroupsService, GroupDirection } from 'vs/workbench/services/group/common/editorGroupsService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; -import { Dimension } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -79,7 +78,7 @@ suite('Editor service', () => { const part = partInstantiator.createInstance(EditorPart, 'id', false); part.create(document.createElement('div')); - part.layout(new Dimension(400, 300)); + part.layout(400, 300); const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); @@ -152,7 +151,7 @@ suite('Editor service', () => { const part = partInstantiator.createInstance(EditorPart, 'id', false); part.create(document.createElement('div')); - part.layout(new Dimension(400, 300)); + part.layout(400, 300); const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); @@ -293,7 +292,7 @@ suite('Editor service', () => { const part = partInstantiator.createInstance(EditorPart, 'id', false); part.create(document.createElement('div')); - part.layout(new Dimension(400, 300)); + part.layout(400, 300); const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); @@ -332,7 +331,7 @@ suite('Editor service', () => { const part = partInstantiator.createInstance(EditorPart, 'id', false); part.create(document.createElement('div')); - part.layout(new Dimension(400, 300)); + part.layout(400, 300); const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); @@ -366,7 +365,7 @@ suite('Editor service', () => { const part = partInstantiator.createInstance(EditorPart, 'id', false); part.create(document.createElement('div')); - part.layout(new Dimension(400, 300)); + part.layout(400, 300); const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); @@ -576,7 +575,7 @@ suite('Editor service', () => { const part = partInstantiator.createInstance(EditorPart, 'id', false); part.create(document.createElement('div')); - part.layout(new Dimension(400, 300)); + part.layout(400, 300); const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index 5a6e51c6d3a..b54507fb37e 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -167,7 +167,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { const opts = { env: objects.mixin(objects.deepClone(process.env), { - AMD_ENTRYPOINT: 'vs/workbench/node/extensionHostProcess', + AMD_ENTRYPOINT: 'vs/workbench/services/extensions/node/extensionHostProcess', PIPE_LOGGING: 'true', VERBOSE_LOGGING: true, VSCODE_IPC_HOOK_EXTHOST: pipeName, diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts index fe57ae9ca52..8329209bdd5 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts @@ -353,7 +353,7 @@ function getLatencyTestProviders(): ExtHostLatencyProvider[] { export class MeasureExtHostLatencyAction extends Action { public static readonly ID = 'editor.action.measureExtHostLatency'; - public static readonly LABEL = nls.localize('measureExtHostLatency', "Developer: Measure Extension Host Latency"); + public static readonly LABEL = nls.localize('measureExtHostLatency', "Measure Extension Host Latency"); constructor( id: string, @@ -384,4 +384,4 @@ export class MeasureExtHostLatencyAction extends Action { } const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(MeasureExtHostLatencyAction, MeasureExtHostLatencyAction.ID, MeasureExtHostLatencyAction.LABEL), 'Developer: Measure Extension Host Latency'); +registry.registerWorkbenchAction(new SyncActionDescriptor(MeasureExtHostLatencyAction, MeasureExtHostLatencyAction.ID, MeasureExtHostLatencyAction.LABEL), 'Developer: Measure Extension Host Latency', nls.localize('developer', "Developer")); diff --git a/src/vs/workbench/services/extensions/electron-browser/inactiveExtensionUrlHandler.ts b/src/vs/workbench/services/extensions/electron-browser/inactiveExtensionUrlHandler.ts index 0b63147da65..f8afb7bc4f5 100644 --- a/src/vs/workbench/services/extensions/electron-browser/inactiveExtensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/electron-browser/inactiveExtensionUrlHandler.ts @@ -190,7 +190,7 @@ export class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { // Extension is not installed else { - const galleryExtension = await this.galleryService.getExtension(extensionIdentifier); + const galleryExtension = await this.galleryService.getCompatibleExtension(extensionIdentifier); if (!galleryExtension) { return; diff --git a/src/vs/workbench/services/extensions/node/extensionDescriptionRegistry.ts b/src/vs/workbench/services/extensions/node/extensionDescriptionRegistry.ts index ad385a196c9..3eddf8c90ed 100644 --- a/src/vs/workbench/services/extensions/node/extensionDescriptionRegistry.ts +++ b/src/vs/workbench/services/extensions/node/extensionDescriptionRegistry.ts @@ -5,8 +5,12 @@ import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { Emitter } from 'vs/base/common/event'; export class ExtensionDescriptionRegistry { + private readonly _onDidChange = new Emitter(); + public readonly onDidChange = this._onDidChange.event; + private _extensionDescriptions: IExtensionDescription[]; private _extensionsMap: Map; private _extensionsArr: IExtensionDescription[]; @@ -53,6 +57,7 @@ export class ExtensionDescriptionRegistry { extensionIds.forEach(extensionId => toKeep.add(ExtensionIdentifier.toKey(extensionId))); this._extensionDescriptions = this._extensionDescriptions.filter(extension => toKeep.has(ExtensionIdentifier.toKey(extension.identifier))); this._initialize(); + this._onDidChange.fire(undefined); } public deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]) { @@ -61,6 +66,7 @@ export class ExtensionDescriptionRegistry { toRemove.forEach(extensionId => toRemoveSet.add(ExtensionIdentifier.toKey(extensionId))); this._extensionDescriptions = this._extensionDescriptions.filter(extension => !toRemoveSet.has(ExtensionIdentifier.toKey(extension.identifier))); this._initialize(); + this._onDidChange.fire(undefined); } public containsActivationEvent(activationEvent: string): boolean { diff --git a/src/vs/workbench/node/extensionHostMain.ts b/src/vs/workbench/services/extensions/node/extensionHostMain.ts similarity index 100% rename from src/vs/workbench/node/extensionHostMain.ts rename to src/vs/workbench/services/extensions/node/extensionHostMain.ts diff --git a/src/vs/workbench/node/extensionHostProcess.ts b/src/vs/workbench/services/extensions/node/extensionHostProcess.ts similarity index 98% rename from src/vs/workbench/node/extensionHostProcess.ts rename to src/vs/workbench/services/extensions/node/extensionHostProcess.ts index ea1b550b065..5f2935618c6 100644 --- a/src/vs/workbench/node/extensionHostProcess.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcess.ts @@ -12,7 +12,7 @@ import { Protocol } from 'vs/base/parts/ipc/node/ipc.net'; import product from 'vs/platform/node/product'; import { IInitData } from 'vs/workbench/api/node/extHost.protocol'; import { MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/common/extensionHostProtocol'; -import { ExtensionHostMain, exit } from 'vs/workbench/node/extensionHostMain'; +import { exit, ExtensionHostMain } from 'vs/workbench/services/extensions/node/extensionHostMain'; // With Electron 2.x and node.js 8.x the "natives" module // can cause a native crash (see https://github.com/nodejs/node/issues/19891 and diff --git a/src/vs/workbench/services/extensions/node/lazyPromise.ts b/src/vs/workbench/services/extensions/node/lazyPromise.ts index df9e72be45c..412a2a2370e 100644 --- a/src/vs/workbench/services/extensions/node/lazyPromise.ts +++ b/src/vs/workbench/services/extensions/node/lazyPromise.ts @@ -82,4 +82,8 @@ export class LazyPromise implements Promise { public catch(error: any): any { return this._ensureActual().then(undefined, error); } + + public finally(callback): any { + return this._ensureActual().finally(callback); + } } diff --git a/src/vs/workbench/node/proxyResolver.ts b/src/vs/workbench/services/extensions/node/proxyResolver.ts similarity index 70% rename from src/vs/workbench/node/proxyResolver.ts rename to src/vs/workbench/services/extensions/node/proxyResolver.ts index c330eb973a2..9205a62dd1b 100644 --- a/src/vs/workbench/node/proxyResolver.ts +++ b/src/vs/workbench/services/extensions/node/proxyResolver.ts @@ -17,6 +17,13 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; import { URI } from 'vs/base/common/uri'; +interface ConnectionResult { + proxy: string; + connection: string; + code: string; + count: number; +} + export function connectProxyResolver( extHostWorkspace: ExtHostWorkspace, configProvider: ExtHostConfigProvider, @@ -24,14 +31,14 @@ export function connectProxyResolver( extHostLogService: ExtHostLogService, mainThreadTelemetry: MainThreadTelemetryShape ) { - const agent = createProxyAgent(extHostWorkspace, configProvider, extHostLogService, mainThreadTelemetry); - const lookup = createPatchedModules(configProvider, agent); + const agents = createProxyAgents(extHostWorkspace, configProvider, extHostLogService, mainThreadTelemetry); + const lookup = createPatchedModules(configProvider, agents); return configureModuleLoading(extensionService, lookup); } const maxCacheEntries = 5000; // Cache can grow twice that much due to 'oldCache'. -function createProxyAgent( +function createProxyAgents( extHostWorkspace: ExtHostWorkspace, configProvider: ExtHostConfigProvider, extHostLogService: ExtHostLogService, @@ -83,6 +90,7 @@ function createProxyAgent( let envCount = 0; let settingsCount = 0; let localhostCount = 0; + let results: ConnectionResult[] = []; function logEvent() { timeout = undefined; /* __GDPR__ @@ -95,14 +103,16 @@ function createProxyAgent( "cacheRolls": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "envCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "settingsCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "localhostCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true } + "localhostCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "results": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } } */ - mainThreadTelemetry.$publicLog('resolveProxy', { count, duration, errorCount, cacheCount, cacheSize: cache.size, cacheRolls, envCount, settingsCount, localhostCount }); + mainThreadTelemetry.$publicLog('resolveProxy', { count, duration, errorCount, cacheCount, cacheSize: cache.size, cacheRolls, envCount, settingsCount, localhostCount, results }); count = duration = errorCount = cacheCount = envCount = settingsCount = localhostCount = 0; + results = []; } - function resolveProxy(url: string, callback: (proxy?: string) => void) { + function resolveProxy(req: http.ClientRequest, opts: http.RequestOptions, url: string, callback: (proxy?: string) => void) { if (!timeout) { timeout = setTimeout(logEvent, 10 * 60 * 1000); } @@ -135,6 +145,7 @@ function createProxyAgent( const proxy = getCachedProxy(key); if (proxy) { cacheCount++; + collectResult(results, proxy, parsedUrl.protocol === 'https:' ? 'HTTPS' : 'HTTP', req); callback(proxy); extHostLogService.trace('ProxyResolver#resolveProxy cached', url, proxy); return; @@ -144,6 +155,7 @@ function createProxyAgent( extHostWorkspace.resolveProxy(url) // Use full URL to ensure it is an actually used one. .then(proxy => { cacheProxy(key, proxy); + collectResult(results, proxy, parsedUrl.protocol === 'https:' ? 'HTTPS' : 'HTTP', req); callback(proxy); extHostLogService.debug('ProxyResolver#resolveProxy', url, proxy); }).then(() => { @@ -156,7 +168,36 @@ function createProxyAgent( }); } - return new ProxyAgent({ resolveProxy }); + const httpAgent: http.Agent = new ProxyAgent({ resolveProxy }); + (httpAgent).defaultPort = 80; + const httpsAgent: http.Agent = new ProxyAgent({ resolveProxy }); + (httpsAgent).defaultPort = 443; + return { http: httpAgent, https: httpsAgent }; +} + +function collectResult(results: ConnectionResult[], resolveProxy: string, connection: string, req: http.ClientRequest) { + const proxy = resolveProxy ? String(resolveProxy).trim().split(/\s+/, 1)[0] : 'EMPTY'; + req.on('response', res => { + const code = `HTTP_${res.statusCode}`; + const result = findOrCreateResult(results, proxy, connection, code); + result.count++; + }); + req.on('error', err => { + const code = err && typeof (err).code === 'string' && (err).code || 'UNKNOWN_ERROR'; + const result = findOrCreateResult(results, proxy, connection, code); + result.count++; + }); +} + +function findOrCreateResult(results: ConnectionResult[], proxy: string, connection: string, code: string): ConnectionResult | undefined { + for (const result of results) { + if (result.proxy === proxy && result.connection === connection && result.code === code) { + return result; + } + } + const result = { proxy, connection, code, count: 0 }; + results.push(result); + return result; } function proxyFromConfigURL(configURL: string) { @@ -177,7 +218,7 @@ function proxyFromConfigURL(configURL: string) { return undefined; } -function createPatchedModules(configProvider: ExtHostConfigProvider, agent: http.Agent) { +function createPatchedModules(configProvider: ExtHostConfigProvider, agents: { http: http.Agent; https: http.Agent; }) { const setting = { config: configProvider.getConfiguration('http') .get('proxySupport') || 'off' @@ -189,25 +230,23 @@ function createPatchedModules(configProvider: ExtHostConfigProvider, agent: http return { http: { - off: assign({}, http, patches(http, agent, { config: 'off' }, true)), - on: assign({}, http, patches(http, agent, { config: 'on' }, true)), - override: assign({}, http, patches(http, agent, { config: 'override' }, true)), - onRequest: assign({}, http, patches(http, agent, setting, true)), - default: assign(http, patches(http, agent, setting, false)) // run last + off: assign({}, http, patches(http, agents.http, agents.https, { config: 'off' }, true)), + on: assign({}, http, patches(http, agents.http, agents.https, { config: 'on' }, true)), + override: assign({}, http, patches(http, agents.http, agents.https, { config: 'override' }, true)), + onRequest: assign({}, http, patches(http, agents.http, agents.https, setting, true)), + default: assign(http, patches(http, agents.http, agents.https, setting, false)) // run last }, https: { - off: assign({}, https, patches(https, agent, { config: 'off' }, true)), - on: assign({}, https, patches(https, agent, { config: 'on' }, true)), - override: assign({}, https, patches(https, agent, { config: 'override' }, true)), - onRequest: assign({}, https, patches(https, agent, setting, true)), - default: assign(https, patches(https, agent, setting, false)) // run last + off: assign({}, https, patches(https, agents.https, agents.http, { config: 'off' }, true)), + on: assign({}, https, patches(https, agents.https, agents.http, { config: 'on' }, true)), + override: assign({}, https, patches(https, agents.https, agents.http, { config: 'override' }, true)), + onRequest: assign({}, https, patches(https, agents.https, agents.http, setting, true)), + default: assign(https, patches(https, agents.https, agents.http, setting, false)) // run last } }; } -function patches(originals: typeof http | typeof https, agent: http.Agent, setting: { config: string; }, onRequest: boolean) { - const defaultPort = originals === https ? 443 : 80; - +function patches(originals: typeof http | typeof https, agent: http.Agent, otherAgent: http.Agent, setting: { config: string; }, onRequest: boolean) { return { get: patch(originals.get), request: patch(originals.request) @@ -231,21 +270,23 @@ function patches(originals: typeof http | typeof https, agent: http.Agent, setti return original.apply(null, arguments as unknown as any[]); } - if (!options.socketPath && (config === 'override' || config === 'on' && !options.agent) && options.agent !== agent) { + if (!options.socketPath && (config === 'override' || config === 'on' && !options.agent) && options.agent !== agent && options.agent !== otherAgent) { if (url) { - const parsed = typeof url === 'string' ? nodeurl.parse(url) : url; - options = { + const parsed = typeof url === 'string' ? new nodeurl.URL(url) : url; + const urlOptions = { protocol: parsed.protocol, - hostname: parsed.hostname, + hostname: parsed.hostname.lastIndexOf('[', 0) === 0 ? parsed.hostname.slice(1, -1) : parsed.hostname, port: parsed.port, - path: parsed.pathname, - ...options + path: `${parsed.pathname}${parsed.search}` }; + if (parsed.username || parsed.password) { + options.auth = `${parsed.username}:${parsed.password}`; + } + options = { ...urlOptions, ...options }; } else { options = { ...options }; } options.agent = agent; - options.defaultPort = defaultPort; // Lets Node's http module omit the port, if it is the default port, in the Host header. (https://github.com/Microsoft/vscode/issues/65118) return original(options, callback); } diff --git a/src/vs/workbench/services/files/electron-browser/streams.ts b/src/vs/workbench/services/files/electron-browser/streams.ts index 60117fe986d..d4af4580a88 100644 --- a/src/vs/workbench/services/files/electron-browser/streams.ts +++ b/src/vs/workbench/services/files/electron-browser/streams.ts @@ -50,7 +50,7 @@ function createWritable(provider: IFileSystemProvider, resource: URI, opts: File async _write(chunk: Buffer, encoding, callback: Function) { try { if (typeof this._fd !== 'number') { - this._fd = await provider.open(resource); + this._fd = await provider.open(resource, { create: true }); } let bytesWritten = await provider.write(this._fd, this._pos, chunk, 0, chunk.length); this._pos += bytesWritten; @@ -60,7 +60,11 @@ function createWritable(provider: IFileSystemProvider, resource: URI, opts: File } } _final(callback: (err?: any) => any) { - provider.close(this._fd).then(callback, callback); + if (typeof this._fd !== 'number') { + provider.open(resource, { create: true }).then(fd => provider.close(fd)).finally(callback); + } else { + provider.close(this._fd).finally(callback); + } } }; } @@ -88,7 +92,7 @@ function createReadable(provider: IFileSystemProvider, resource: URI, position: this._reading = true; try { if (typeof this._fd !== 'number') { - this._fd = await provider.open(resource); + this._fd = await provider.open(resource, { create: false }); } while (this._reading) { let buffer = Buffer.allocUnsafe(size); diff --git a/src/vs/workbench/services/group/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/group/test/browser/editorGroupsService.test.ts index d9a1a1f8c33..806b62a5e55 100644 --- a/src/vs/workbench/services/group/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/group/test/browser/editorGroupsService.test.ts @@ -7,7 +7,6 @@ import * as assert from 'assert'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupChangeKind, EditorsOrder, GroupLocation } from 'vs/workbench/services/group/common/editorGroupsService'; -import { Dimension } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorPartOptions } from 'vs/workbench/browser/parts/editor/editor'; import { EditorInput, IFileEditorInput, IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorExtensions, EditorOptions, CloseDirection } from 'vs/workbench/common/editor'; @@ -90,7 +89,7 @@ suite('Editor groups service', () => { const part = instantiationService.createInstance(EditorPart, 'id', false); part.create(document.createElement('div')); - part.layout(new Dimension(400, 300)); + part.layout(400, 300); return part; } diff --git a/src/vs/workbench/services/history/electron-browser/history.ts b/src/vs/workbench/services/history/electron-browser/history.ts index ca31a8508d2..e1cae269b8b 100644 --- a/src/vs/workbench/services/history/electron-browser/history.ts +++ b/src/vs/workbench/services/history/electron-browser/history.ts @@ -30,7 +30,6 @@ import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { coalesce } from 'vs/base/common/arrays'; -import { always } from 'vs/base/common/async'; /** * Stores the selection & view state of an editor and allows to compare it to other selection states. @@ -408,7 +407,7 @@ export class HistoryService extends Disposable implements IHistoryService { private navigate(acrossEditors?: boolean): void { this.navigatingInStack = true; - always(this.doNavigate(this.stack[this.index], !acrossEditors), () => this.navigatingInStack = false); + this.doNavigate(this.stack[this.index], !acrossEditors).finally(() => this.navigatingInStack = false); } private doNavigate(location: IStackEntry, withSelection: boolean): Promise { diff --git a/src/vs/workbench/services/part/common/partService.ts b/src/vs/workbench/services/part/common/partService.ts index 23d75ea37e1..6c63edf6559 100644 --- a/src/vs/workbench/services/part/common/partService.ts +++ b/src/vs/workbench/services/part/common/partService.ts @@ -86,6 +86,12 @@ export interface IPartService { */ getTitleBarOffset(): number; + /** + * + * Set editor area hidden or not + */ + setEditorHidden(hidden: boolean): void; + /** * Set sidebar hidden or not */ diff --git a/src/vs/workbench/services/progress/browser/media/progressService2.css b/src/vs/workbench/services/progress/browser/media/progressService2.css index a841cb76c2f..850fdf2e3c5 100644 --- a/src/vs/workbench/services/progress/browser/media/progressService2.css +++ b/src/vs/workbench/services/progress/browser/media/progressService2.css @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench > .part.statusbar > .statusbar-item.progress { +.monaco-workbench .part.statusbar > .statusbar-item.progress { padding-left: 5px; } -.monaco-workbench > .part.statusbar > .statusbar-item.progress .spinner-container { +.monaco-workbench .part.statusbar > .statusbar-item.progress .spinner-container { padding-right: 5px; } diff --git a/src/vs/workbench/services/progress/browser/progressService2.ts b/src/vs/workbench/services/progress/browser/progressService2.ts index 606793db659..da77ec66b8d 100644 --- a/src/vs/workbench/services/progress/browser/progressService2.ts +++ b/src/vs/workbench/services/progress/browser/progressService2.ts @@ -10,7 +10,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IProgressService2, IProgressOptions, IProgressStep, ProgressLocation, IProgress, emptyProgress, Progress } from 'vs/platform/progress/common/progress'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { StatusbarAlignment, IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; -import { always, timeout } from 'vs/base/common/async'; +import { timeout } from 'vs/base/common/async'; import { ProgressBadge, IActivityService } from 'vs/workbench/services/activity/common/activity'; import { INotificationService, Severity, INotificationHandle, INotificationActions } from 'vs/platform/notification/common/notification'; import { Action } from 'vs/base/common/actions'; @@ -30,7 +30,7 @@ export class ProgressService2 implements IProgressService2 { @IStatusbarService private readonly _statusbarService: IStatusbarService, ) { } - withProgress

, R=any>(options: IProgressOptions, task: (progress: IProgress) => P, onDidCancel?: () => void): P { + withProgress(options: IProgressOptions, task: (progress: IProgress) => Promise, onDidCancel?: () => void): Promise { const { location } = options; if (typeof location === 'string') { @@ -59,7 +59,7 @@ export class ProgressService2 implements IProgressService2 { } } - private _withWindowProgress

, R=any>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string }>) => P): P { + private _withWindowProgress(options: IProgressOptions, callback: (progress: IProgress<{ message?: string }>) => Promise): Promise { const task: [IProgressOptions, Progress] = [options, new Progress(() => this._updateWindowProgress())]; @@ -71,10 +71,10 @@ export class ProgressService2 implements IProgressService2 { this._updateWindowProgress(); // show progress for at least 150ms - always(Promise.all([ + Promise.all([ timeout(150), promise - ]), () => { + ]).finally(() => { const idx = this._stack.indexOf(task); this._stack.splice(idx, 1); this._updateWindowProgress(); @@ -83,8 +83,7 @@ export class ProgressService2 implements IProgressService2 { }, 150); // cancel delay if promise finishes below 150ms - always(promise, () => clearTimeout(delayHandle)); - return promise; + return promise.finally(() => clearTimeout(delayHandle)); } private _updateWindowProgress(idx: number = 0) { @@ -214,7 +213,7 @@ export class ProgressService2 implements IProgressService2 { }); // Show progress for at least 800ms and then hide once done or canceled - always(Promise.all([timeout(800), p]), () => { + Promise.all([timeout(800), p]).finally(() => { if (handle) { handle.close(); } diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 138360c0ccc..e656f69ccc6 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -25,13 +25,6 @@ import { IFileQuery, IFolderQuery, IProgress, ISearchEngineStats } from 'vs/plat import { IRawFileMatch, ISearchEngine, ISearchEngineSuccess } from 'vs/workbench/services/search/node/search'; import { spawnRipgrepCmd } from './ripgrepFileSearch'; -enum Traversal { - Node = 1, - MacFind, - LinuxFind, - Ripgrep -} - interface IDirectoryEntry { base: string; relativePath: string; @@ -50,7 +43,6 @@ process.on('exit', () => { export class FileWalker { private config: IFileQuery; - private useRipgrep: boolean; private filePattern: string; private normalizedFilePatternLowercase: string; private includePattern: glob.ParsedExpression | undefined; @@ -63,7 +55,6 @@ export class FileWalker { private fileWalkSW: StopWatch; private directoriesWalked: number; private filesWalked: number; - private traversal: Traversal; private errors: string[]; private cmdSW: StopWatch; private cmdResultCount: number; @@ -73,20 +64,17 @@ export class FileWalker { private walkedPaths: { [path: string]: boolean; }; - constructor(config: IFileQuery, maxFileSize?: number) { + constructor(config: IFileQuery) { this.config = config; - this.useRipgrep = config.useRipgrep !== false; this.filePattern = config.filePattern || ''; this.includePattern = config.includePattern && glob.parse(config.includePattern); this.maxResults = config.maxResults || null; this.exists = !!config.exists; - this.maxFilesize = maxFileSize || null; this.walkedPaths = Object.create(null); this.resultCount = 0; this.isLimitHit = false; this.directoriesWalked = 0; this.filesWalked = 0; - this.traversal = Traversal.Node; this.errors = []; if (this.filePattern) { @@ -138,28 +126,11 @@ export class FileWalker { this.matchFile(onResult, { relativePath: extraFilePath.fsPath /* no workspace relative path */, basename }); }); - let traverse = this.nodeJSTraversal; - if (!this.maxFilesize) { - if (this.useRipgrep) { - this.traversal = Traversal.Ripgrep; - traverse = this.cmdTraversal; - } else if (platform.isMacintosh) { - this.traversal = Traversal.MacFind; - traverse = this.cmdTraversal; - } else if (platform.isLinux) { - this.traversal = Traversal.LinuxFind; - traverse = this.cmdTraversal; - } - } - - const isNodeTraversal = traverse === this.nodeJSTraversal; - if (!isNodeTraversal) { - this.cmdSW = StopWatch.create(false); - } + this.cmdSW = StopWatch.create(false); // For each root folder flow.parallel(folderQueries, (folderQuery: IFolderQuery, rootFolderDone: (err: Error | null, result: void) => void) => { - this.call(traverse, this, folderQuery, onResult, onMessage, (err?: Error) => { + this.call(this.cmdTraversal, this, folderQuery, onResult, onMessage, (err?: Error) => { if (err) { const errorMessage = toErrorMessage(err); console.error(errorMessage); @@ -197,31 +168,25 @@ export class FileWalker { cb(err); }; let leftover = ''; - let first = true; const tree = this.initDirectoryTree(); - const useRipgrep = this.useRipgrep; let noSiblingsClauses: boolean; - if (useRipgrep) { - const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder.fsPath)!.expression); - cmd = ripgrep.cmd; - noSiblingsClauses = !Object.keys(ripgrep.siblingClauses).length; + const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder.fsPath)!.expression); + cmd = ripgrep.cmd; + noSiblingsClauses = !Object.keys(ripgrep.siblingClauses).length; - const escapedArgs = ripgrep.rgArgs.args - .map(arg => arg.match(/^-/) ? arg : `'${arg}'`) - .join(' '); + const escapedArgs = ripgrep.rgArgs.args + .map(arg => arg.match(/^-/) ? arg : `'${arg}'`) + .join(' '); - let rgCmd = `rg ${escapedArgs}\n - cwd: ${ripgrep.cwd}`; - if (ripgrep.rgArgs.siblingClauses) { - rgCmd += `\n - Sibling clauses: ${JSON.stringify(ripgrep.rgArgs.siblingClauses)}`; - } - onMessage({ message: rgCmd }); - } else { - cmd = this.spawnFindCmd(folderQuery); + let rgCmd = `rg ${escapedArgs}\n - cwd: ${ripgrep.cwd}`; + if (ripgrep.rgArgs.siblingClauses) { + rgCmd += `\n - Sibling clauses: ${JSON.stringify(ripgrep.rgArgs.siblingClauses)}`; } + onMessage({ message: rgCmd }); this.cmdResultCount = 0; - this.collectStdout(cmd, 'utf8', useRipgrep, onMessage, (err: Error, stdout?: string, last?: boolean) => { + this.collectStdout(cmd, 'utf8', onMessage, (err: Error, stdout?: string, last?: boolean) => { if (err) { done(err); return; @@ -233,11 +198,7 @@ export class FileWalker { // Mac: uses NFD unicode form on disk, but we want NFC const normalized = leftover + (isMac ? normalization.normalizeNFC(stdout || '') : stdout); - const relativeFiles = normalized.split(useRipgrep ? '\n' : '\n./'); - if (!useRipgrep && first && normalized.length >= 2) { - first = false; - relativeFiles[0] = relativeFiles[0].trim().substr(2); - } + const relativeFiles = normalized.split('\n'); if (last) { const n = relativeFiles.length; @@ -256,7 +217,7 @@ export class FileWalker { this.cmdResultCount += relativeFiles.length; - if (useRipgrep && noSiblingsClauses) { + if (noSiblingsClauses) { for (const relativePath of relativeFiles) { const basename = path.basename(relativePath); this.matchFile(onResult, { base: rootFolder, relativePath, basename }); @@ -310,9 +271,9 @@ export class FileWalker { /** * Public for testing. */ - readStdout(cmd: childProcess.ChildProcess, encoding: string, isRipgrep: boolean, cb: (err: Error | null, stdout?: string) => void): void { + readStdout(cmd: childProcess.ChildProcess, encoding: string, cb: (err: Error | null, stdout?: string) => void): void { let all = ''; - this.collectStdout(cmd, encoding, isRipgrep, () => { }, (err: Error, stdout?: string, last?: boolean) => { + this.collectStdout(cmd, encoding, () => { }, (err: Error, stdout?: string, last?: boolean) => { if (err) { cb(err); return; @@ -325,7 +286,7 @@ export class FileWalker { }); } - private collectStdout(cmd: childProcess.ChildProcess, encoding: string, isRipgrep: boolean, onMessage: (message: IProgress) => void, cb: (err: Error | null, stdout?: string, last?: boolean) => void): void { + private collectStdout(cmd: childProcess.ChildProcess, encoding: string, onMessage: (message: IProgress) => void, cb: (err: Error | null, stdout?: string, last?: boolean) => void): void { let onData = (err: Error | null, stdout?: string, last?: boolean) => { if (err || last) { onData = () => { }; @@ -361,10 +322,10 @@ export class FileWalker { cmd.on('close', (code: number) => { // ripgrep returns code=1 when no results are found let stderrText: string; - if (isRipgrep ? (!gotData && (stderrText = this.decodeData(stderr, encoding)) && rgErrorMsgForDisplay(stderrText)) : code !== 0) { + if (!gotData && (stderrText = this.decodeData(stderr, encoding)) && rgErrorMsgForDisplay(stderrText)) { onData(new Error(`command failed with error code ${code}: ${this.decodeData(stderr, encoding)}`)); } else { - if (isRipgrep && this.exists && code === 0) { + if (this.exists && code === 0) { this.isLimitHit = true; } onData(null, '', true); @@ -465,26 +426,10 @@ export class FileWalker { matchDirectory(rootEntries); } - private nodeJSTraversal(folderQuery: IFolderQuery, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgress) => void, done: (err?: Error) => void): void { - this.directoriesWalked++; - extfs.readdir(folderQuery.folder.fsPath, (error: Error, files: string[]) => { - if (error || this.isCanceled || this.isLimitHit) { - return done(); - } - - if (this.isCanceled || this.isLimitHit) { - return done(); - } - - return this.doWalk(folderQuery, '', files, onResult, done); - }); - } - getStats(): ISearchEngineStats { return { cmdTime: this.cmdSW && this.cmdSW.elapsed(), fileWalkTime: this.fileWalkSW.elapsed(), - traversal: Traversal[this.traversal], directoriesWalked: this.directoriesWalked, filesWalked: this.filesWalked, cmdResultCount: this.cmdResultCount diff --git a/src/vs/workbench/services/search/node/legacy/rawLegacyTextSearchService.ts b/src/vs/workbench/services/search/node/legacy/rawLegacyTextSearchService.ts deleted file mode 100644 index e7189c56c12..00000000000 --- a/src/vs/workbench/services/search/node/legacy/rawLegacyTextSearchService.ts +++ /dev/null @@ -1,75 +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 fs from 'fs'; -import * as gracefulFs from 'graceful-fs'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { MAX_FILE_SIZE } from 'vs/platform/files/node/files'; -import { ITextQuery, QueryType } from 'vs/platform/search/common/search'; -import { FileWalker } from 'vs/workbench/services/search/node/fileSearch'; -import { Engine } from 'vs/workbench/services/search/node/legacy/textSearch'; -import { TextSearchWorkerProvider } from 'vs/workbench/services/search/node/legacy/textSearchWorkerProvider'; -import { BatchedCollector } from 'vs/workbench/services/search/node/textSearchManager'; -import { ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess } from '../search'; - -gracefulFs.gracefulify(fs); - -type IProgressCallback = (p: ISerializedSearchProgressItem) => void; - -export class LegacyTextSearchService { - private static readonly BATCH_SIZE = 512; - - private textSearchWorkerProvider: TextSearchWorkerProvider; - - textSearch(config: ITextQuery, progressCallback: IProgressCallback, token?: CancellationToken): Promise { - if (!this.textSearchWorkerProvider) { - this.textSearchWorkerProvider = new TextSearchWorkerProvider(); - } - - let engine = new Engine( - config, - new FileWalker({ - type: QueryType.File, - folderQueries: config.folderQueries, - extraFileResources: config.extraFileResources, - includePattern: config.includePattern, - excludePattern: config.excludePattern, - useRipgrep: false, - filePattern: '' - }, MAX_FILE_SIZE), - this.textSearchWorkerProvider); - - return this.doTextSearch(engine, progressCallback, LegacyTextSearchService.BATCH_SIZE, token); - } - - private doTextSearch(engine: Engine, progressCallback: IProgressCallback, batchSize: number, token?: CancellationToken): Promise { - if (token) { - token.onCancellationRequested(() => engine.cancel()); - } - - return new Promise((c, e) => { - // Use BatchedCollector to get new results to the frontend every 2s at least, until 50 results have been returned - const collector = new BatchedCollector(batchSize, progressCallback); - engine.search((matches) => { - const totalMatches = matches.reduce((acc, m) => acc + m.numMatches!, 0); - collector.addItems(matches, totalMatches); - }, (progress) => { - progressCallback(progress); - }, (error, stats) => { - collector.flush(); - - if (error) { - e(error); - } else { - c({ - type: 'success', - limitHit: stats.limitHit, - stats: null - }); - } - }); - }); - } -} diff --git a/src/vs/workbench/services/search/node/legacy/search.ts b/src/vs/workbench/services/search/node/legacy/search.ts deleted file mode 100644 index 84cac9d4f5a..00000000000 --- a/src/vs/workbench/services/search/node/legacy/search.ts +++ /dev/null @@ -1,35 +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 glob from 'vs/base/common/glob'; -import { IPatternInfo, ITextSearchPreviewOptions } from 'vs/platform/search/common/search'; - -export interface IFolderSearch { - folder: string; - excludePattern?: glob.IExpression; - includePattern?: glob.IExpression; - fileEncoding?: string; - disregardIgnoreFiles?: boolean; - disregardGlobalIgnoreFiles?: boolean; -} - -export interface IRawSearch { - folderQueries: IFolderSearch[]; - ignoreSymlinks?: boolean; - extraFiles?: string[]; - filePattern?: string; - excludePattern?: glob.IExpression; - includePattern?: glob.IExpression; - contentPattern: IPatternInfo; - maxResults?: number; - exists?: boolean; - sortByScore?: boolean; - cacheKey?: string; - maxFilesize?: number; - useRipgrep?: boolean; - disregardIgnoreFiles?: boolean; - previewOptions?: ITextSearchPreviewOptions; - disregardGlobalIgnoreFiles?: boolean; -} diff --git a/src/vs/workbench/services/search/node/legacy/textSearch.ts b/src/vs/workbench/services/search/node/legacy/textSearch.ts deleted file mode 100644 index 0c8fa95a224..00000000000 --- a/src/vs/workbench/services/search/node/legacy/textSearch.ts +++ /dev/null @@ -1,205 +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 path from 'path'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { IProgress, ITextQuery } from 'vs/platform/search/common/search'; -import { FileWalker } from 'vs/workbench/services/search/node/fileSearch'; -import { ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch } from '../search'; -import { ITextSearchWorkerProvider } from './textSearchWorkerProvider'; -import { ISearchWorker, ISearchWorkerSearchArgs } from './worker/searchWorkerIpc'; -import { IRawSearch } from 'vs/workbench/services/search/node/legacy/search'; - -export class Engine implements ISearchEngine { - - private static readonly PROGRESS_FLUSH_CHUNK_SIZE = 50; // optimization: number of files to process before emitting progress event - - private config: IRawSearch; - private config2: ITextQuery; - private walker: FileWalker; - private walkerError: Error | null; - - private isCanceled = false; - private isDone = false; - private totalBytes = 0; - private processedBytes = 0; - private progressed = 0; - private walkerIsDone = false; - private limitReached = false; - private numResults = 0; - - private workerProvider: ITextSearchWorkerProvider; - private workers: ISearchWorker[]; - - private nextWorker = 0; - - constructor(config: ITextQuery, walker: FileWalker, workerProvider: ITextSearchWorkerProvider) { - this.config = makeRawSearch(config); - this.config2 = config; - this.walker = walker; - this.workerProvider = workerProvider; - } - - cancel(): void { - this.isCanceled = true; - this.walker.cancel(); - - this.workers.forEach(w => { - w.cancel() - .then(undefined, onUnexpectedError); - }); - } - - initializeWorkers(): void { - this.workers.forEach(w => { - w.initialize() - .then(undefined, onUnexpectedError); - }); - } - - search(onResult: (match: ISerializedFileMatch[]) => void, onProgress: (progress: IProgress) => void, done: (error: Error | null, complete: ISearchEngineSuccess) => void): void { - this.workers = this.workerProvider.getWorkers(); - this.initializeWorkers(); - - const fileEncoding = this.config.folderQueries.length === 1 ? - this.config.folderQueries[0].fileEncoding || 'utf8' : - 'utf8'; - - const progress = () => { - if (++this.progressed % Engine.PROGRESS_FLUSH_CHUNK_SIZE === 0) { - onProgress({ total: this.totalBytes, worked: this.processedBytes }); // buffer progress in chunks to reduce pressure - } - }; - - const unwind = (processed: number) => { - this.processedBytes += processed; - - // Emit progress() unless we got canceled or hit the limit - if (processed && !this.isDone && !this.isCanceled && !this.limitReached) { - progress(); - } - - // Emit done() - if (!this.isDone && this.processedBytes === this.totalBytes && this.walkerIsDone) { - this.isDone = true; - done(this.walkerError, { - limitHit: this.limitReached, - stats: this.walker.getStats() - }); - } - }; - - const run = (batch: string[], batchBytes: number): void => { - const worker = this.workers[this.nextWorker]; - this.nextWorker = (this.nextWorker + 1) % this.workers.length; - - const maxResults = this.config.maxResults && (this.config.maxResults - this.numResults); - const searchArgs: ISearchWorkerSearchArgs = { absolutePaths: batch, maxResults, pattern: this.config.contentPattern, fileEncoding, previewOptions: this.config.previewOptions }; - worker.search(searchArgs).then(result => { - if (!result || this.limitReached || this.isCanceled) { - return unwind(batchBytes); - } - - const matches = result.matches; - onResult(matches); - this.numResults += result.numMatches; - - if (this.config.maxResults && this.numResults >= this.config.maxResults) { - // It's possible to go over maxResults like this, but it's much simpler than trying to extract the exact number - // of file matches, line matches, and matches within a line to == maxResults. - this.limitReached = true; - } - - unwind(batchBytes); - }, - error => { - // An error on the worker's end, not in reading the file, but in processing the batch. Log and continue. - onUnexpectedError(error); - unwind(batchBytes); - }); - }; - - // Walk over the file system - let nextBatch: string[] = []; - let nextBatchBytes = 0; - const batchFlushBytes = 2 ** 20; // 1MB - this.walker.walk(this.config2.folderQueries, this.config2.extraFileResources || [], result => { - let bytes = result.size || 1; - this.totalBytes += bytes; - - // If we have reached the limit or we are canceled, ignore it - if (this.limitReached || this.isCanceled) { - return unwind(bytes); - } - - // Indicate progress to the outside - progress(); - - const absolutePath = result.base ? [result.base, result.relativePath].join(path.sep) : result.relativePath; - nextBatch.push(absolutePath); - nextBatchBytes += bytes; - - if (nextBatchBytes >= batchFlushBytes) { - run(nextBatch, nextBatchBytes); - nextBatch = []; - nextBatchBytes = 0; - } - }, - onProgress, - (error, isLimitHit) => { - this.walkerIsDone = true; - this.walkerError = error; - - // Send any remaining paths to a worker, or unwind if we're stopping - if (nextBatch.length) { - if (this.limitReached || this.isCanceled) { - unwind(nextBatchBytes); - } else { - run(nextBatch, nextBatchBytes); - } - } else { - unwind(0); - } - }); - } -} - -/** - * Exported for tests - */ -export function makeRawSearch(query: ITextQuery): IRawSearch { - let rawSearch: IRawSearch = { - folderQueries: [], - extraFiles: [], - excludePattern: query.excludePattern, - includePattern: query.includePattern, - maxResults: query.maxResults, - useRipgrep: query.useRipgrep, - disregardIgnoreFiles: query.folderQueries.some(fq => fq.disregardIgnoreFiles!), - disregardGlobalIgnoreFiles: query.folderQueries.some(fq => fq.disregardGlobalIgnoreFiles!), - ignoreSymlinks: query.folderQueries.some(fq => fq.ignoreSymlinks!), - previewOptions: query.previewOptions, - contentPattern: query.contentPattern - }; - - for (const q of query.folderQueries) { - rawSearch.folderQueries.push({ - excludePattern: q.excludePattern, - includePattern: q.includePattern, - fileEncoding: q.fileEncoding, - disregardIgnoreFiles: q.disregardIgnoreFiles, - disregardGlobalIgnoreFiles: q.disregardGlobalIgnoreFiles, - folder: q.folder.fsPath - }); - } - - if (query.extraFileResources) { - for (const r of query.extraFileResources) { - rawSearch.extraFiles!.push(r.fsPath); - } - } - - return rawSearch; -} diff --git a/src/vs/workbench/services/search/node/legacy/textSearchWorkerProvider.ts b/src/vs/workbench/services/search/node/legacy/textSearchWorkerProvider.ts deleted file mode 100644 index ac848db101e..00000000000 --- a/src/vs/workbench/services/search/node/legacy/textSearchWorkerProvider.ts +++ /dev/null @@ -1,50 +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 os from 'os'; - -import * as ipc from 'vs/base/parts/ipc/node/ipc'; -import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; - -import { ISearchWorker, SearchWorkerChannelClient } from './worker/searchWorkerIpc'; -import { getPathFromAmdModule } from 'vs/base/common/amd'; - -export interface ITextSearchWorkerProvider { - getWorkers(): ISearchWorker[]; -} - -export class TextSearchWorkerProvider implements ITextSearchWorkerProvider { - private workers: ISearchWorker[] = []; - - getWorkers(): ISearchWorker[] { - const numWorkers = os.cpus().length; - while (this.workers.length < numWorkers) { - this.createWorker(); - } - - return this.workers; - } - - private createWorker(): void { - let client = new Client( - getPathFromAmdModule(require, 'bootstrap-fork'), - { - serverName: 'Search Worker ' + this.workers.length, - args: ['--type=searchWorker'], - timeout: 30 * 1000, - env: { - AMD_ENTRYPOINT: 'vs/workbench/services/search/node/legacy/worker/searchWorkerApp', - PIPE_LOGGING: 'true', - VERBOSE_LOGGING: process.env.VERBOSE_LOGGING - }, - useQueue: true - }); - - const channel = ipc.getNextTickChannel(client.getChannel('searchWorker')); - const channelClient = new SearchWorkerChannelClient(channel); - - this.workers.push(channelClient); - } -} diff --git a/src/vs/workbench/services/search/node/legacy/worker/searchWorker.ts b/src/vs/workbench/services/search/node/legacy/worker/searchWorker.ts deleted file mode 100644 index 084455e1e46..00000000000 --- a/src/vs/workbench/services/search/node/legacy/worker/searchWorker.ts +++ /dev/null @@ -1,291 +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 fs from 'fs'; -import * as gracefulFs from 'graceful-fs'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import * as strings from 'vs/base/common/strings'; -import { bomLength, decode, detectEncodingFromBuffer, encodingExists, UTF16be, UTF16le, UTF8, UTF8_with_bom } from 'vs/base/node/encoding'; -import { Range } from 'vs/editor/common/core/range'; -import { ITextSearchPreviewOptions, TextSearchMatch } from 'vs/platform/search/common/search'; -import { ISearchWorker, ISearchWorkerSearchArgs, ISearchWorkerSearchResult } from './searchWorkerIpc'; -import { FileMatch } from 'vs/workbench/services/search/node/search'; - -gracefulFs.gracefulify(fs); - -interface ReadLinesOptions { - bufferLength: number; - encoding: string; -} - -const MAX_FILE_ERRORS = 5; // Don't report more than this number of errors, 1 per file, to avoid flooding the log when there's a general issue -let numErrorsLogged = 0; -function onError(error: any): void { - if (numErrorsLogged++ < MAX_FILE_ERRORS) { - onUnexpectedError(error); - } -} - -export class SearchWorker implements ISearchWorker { - private currentSearchEngine: SearchWorkerEngine; - - initialize(): Promise { - this.currentSearchEngine = new SearchWorkerEngine(); - return Promise.resolve(undefined); - } - - cancel(): Promise { - // Cancel the current search. It will stop searching and close its open files. - if (this.currentSearchEngine) { - this.currentSearchEngine.cancel(); - } - - return Promise.resolve(undefined); - } - - search(args: ISearchWorkerSearchArgs): Promise { - if (!this.currentSearchEngine) { - // Worker timed out during search - this.initialize(); - } - - return this.currentSearchEngine.searchBatch(args); - } -} - -interface IFileSearchResult { - match: FileMatch; - numMatches: number; - limitReached?: boolean; -} - -const LF = 0x0A; -const CR = 0x0D; - -export class SearchWorkerEngine { - private nextSearch: Promise = Promise.resolve(null); - private isCanceled = false; - - /** - * Searches some number of the given paths concurrently, and starts searches in other paths when those complete. - */ - searchBatch(args: ISearchWorkerSearchArgs): Promise { - const contentPattern = strings.createRegExp(args.pattern.pattern, !!args.pattern.isRegExp, { matchCase: args.pattern.isCaseSensitive, wholeWord: args.pattern.isWordMatch, multiline: false, global: true }); - const fileEncoding = encodingExists(args.fileEncoding) ? args.fileEncoding : UTF8; - return this.nextSearch = - this.nextSearch.then(() => this._searchBatch(args, contentPattern, fileEncoding)); - } - - - private _searchBatch(args: ISearchWorkerSearchArgs, contentPattern: RegExp, fileEncoding: string): Promise { - if (this.isCanceled) { - return Promise.resolve(null); - } - - return new Promise(batchDone => { - const result: ISearchWorkerSearchResult = { - matches: [], - numMatches: 0, - limitReached: false - }; - - // Search in the given path, and when it's finished, search in the next path in absolutePaths - const startSearchInFile = (absolutePath: string): Promise => { - return this.searchInFile(absolutePath, contentPattern, fileEncoding, args.maxResults && (args.maxResults - result.numMatches), args.previewOptions).then(fileResult => { - // Finish early if search is canceled - if (this.isCanceled) { - return; - } - - if (fileResult) { - result.numMatches += fileResult.numMatches; - result.matches.push(fileResult.match.serialize()); - if (fileResult.limitReached) { - // If the limit was reached, terminate early with the results so far and cancel in-progress searches. - this.cancel(); - result.limitReached = true; - return batchDone(result); - } - } - }, onError); - }; - - Promise.all(args.absolutePaths.map(startSearchInFile)).then(() => { - batchDone(result); - }); - }); - } - - cancel(): void { - this.isCanceled = true; - } - - private searchInFile(absolutePath: string, contentPattern: RegExp, fileEncoding: string, maxResults?: number, previewOptions?: ITextSearchPreviewOptions): Promise { - let fileMatch: FileMatch | null = null; - let limitReached = false; - let numMatches = 0; - - const perLineCallback = (line: string, lineNumber: number) => { - let match = contentPattern.exec(line); - - // Record all matches into file result - while (match !== null && match[0].length > 0 && !this.isCanceled && !limitReached) { - if (fileMatch === null) { - fileMatch = new FileMatch(absolutePath); - } - - const lineMatch = new TextSearchMatch(line, new Range(lineNumber, match.index, lineNumber, match.index + match[0].length), previewOptions); - fileMatch.addMatch(lineMatch); - - numMatches++; - if (maxResults && numMatches >= maxResults) { - limitReached = true; - } - - match = contentPattern.exec(line); - } - }; - - // Read lines buffered to support large files - return this.readlinesAsync(absolutePath, perLineCallback, { bufferLength: 8096, encoding: fileEncoding }).then( - () => fileMatch ? { match: fileMatch, limitReached, numMatches } : null); - } - - private readlinesAsync(filename: string, perLineCallback: (line: string, lineNumber: number) => void, options: ReadLinesOptions): Promise { - return new Promise((resolve, reject) => { - fs.open(filename, 'r', null, (error: Error, fd: number) => { - if (error) { - return resolve(undefined); - } - - const buffer = Buffer.allocUnsafe(options.bufferLength); - let line = ''; - let lineNumber = 0; - let lastBufferHadTrailingCR = false; - - const readFile = (isFirstRead: boolean, clb: (error: Error | null) => void): void => { - if (this.isCanceled) { - return clb(null); // return early if canceled or limit reached - } - - fs.read(fd, buffer, 0, buffer.length, null, (error: Error, bytesRead: number, buffer: Buffer) => { - const decodeBuffer = (buffer: Buffer, start: number, end: number): string => { - if (options.encoding === UTF8 || options.encoding === UTF8_with_bom) { - return buffer.toString(undefined, start, end); // much faster to use built in toString() when encoding is default - } - - return decode(buffer.slice(start, end), options.encoding); - }; - - const lineFinished = (offset: number): void => { - line += decodeBuffer(buffer, pos, i + offset); - perLineCallback(line, lineNumber); - line = ''; - lineNumber++; - pos = i + offset; - }; - - if (error || bytesRead === 0 || this.isCanceled) { - return clb(error); // return early if canceled or limit reached or no more bytes to read - } - - let crlfCharSize = 1; - let crBytes = [CR]; - let lfBytes = [LF]; - let pos = 0; - let i = 0; - - // Detect encoding and mime when this is the beginning of the file - if (isFirstRead) { - const detected = detectEncodingFromBuffer({ buffer, bytesRead }, false); - if (detected.seemsBinary) { - return clb(null); // skip files that seem binary - } - - // Check for BOM offset - switch (detected.encoding) { - case UTF8: - pos = i = bomLength(UTF8); - options.encoding = UTF8; - break; - case UTF16be: - pos = i = bomLength(UTF16be); - options.encoding = UTF16be; - break; - case UTF16le: - pos = i = bomLength(UTF16le); - options.encoding = UTF16le; - break; - } - - // when we are running with UTF16le/be, LF and CR are encoded as - // two bytes, like 0A 00 (LF) / 0D 00 (CR) for LE or flipped around - // for BE. We need to account for this when splitting the buffer into - // newlines, and when detecting a CRLF combo. - if (options.encoding === UTF16le) { - crlfCharSize = 2; - crBytes = [CR, 0x00]; - lfBytes = [LF, 0x00]; - } else if (options.encoding === UTF16be) { - crlfCharSize = 2; - crBytes = [0x00, CR]; - lfBytes = [0x00, LF]; - } - } - - if (lastBufferHadTrailingCR) { - if (buffer[i] === lfBytes[0] && (lfBytes.length === 1 || buffer[i + 1] === lfBytes[1])) { - lineFinished(1 * crlfCharSize); - i++; - } else { - lineFinished(0); - } - - lastBufferHadTrailingCR = false; - } - - /** - * This loop executes for every byte of every file in the workspace - it is highly performance-sensitive! - * Hence the duplication in reading the buffer to avoid a function call. Previously a function call was not - * being inlined by V8. - */ - for (; i < bytesRead; ++i) { - if (buffer[i] === lfBytes[0] && (lfBytes.length === 1 || buffer[i + 1] === lfBytes[1])) { - lineFinished(1 * crlfCharSize); - } else if (buffer[i] === crBytes[0] && (crBytes.length === 1 || buffer[i + 1] === crBytes[1])) { // CR (Carriage Return) - if (i + crlfCharSize === bytesRead) { - lastBufferHadTrailingCR = true; - } else if (buffer[i + crlfCharSize] === lfBytes[0] && (lfBytes.length === 1 || buffer[i + crlfCharSize + 1] === lfBytes[1])) { - lineFinished(2 * crlfCharSize); - i += 2 * crlfCharSize - 1; - } else { - lineFinished(1 * crlfCharSize); - } - } - } - - line += decodeBuffer(buffer, pos, bytesRead); - - readFile(/*isFirstRead=*/false, clb); // Continue reading - }); - }; - - readFile(/*isFirstRead=*/true, (error: Error) => { - if (error) { - return resolve(undefined); - } - - if (line.length) { - perLineCallback(line, lineNumber); // handle last line - } - - fs.close(fd, (error: Error) => { - resolve(undefined); - }); - }); - }); - }); - } -} diff --git a/src/vs/workbench/services/search/node/legacy/worker/searchWorkerApp.ts b/src/vs/workbench/services/search/node/legacy/worker/searchWorkerApp.ts deleted file mode 100644 index 3abda129ab4..00000000000 --- a/src/vs/workbench/services/search/node/legacy/worker/searchWorkerApp.ts +++ /dev/null @@ -1,13 +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 { Server } from 'vs/base/parts/ipc/node/ipc.cp'; -import { SearchWorkerChannel } from './searchWorkerIpc'; -import { SearchWorker } from './searchWorker'; - -const server = new Server('searchWorker'); -const worker = new SearchWorker(); -const channel = new SearchWorkerChannel(worker); -server.registerChannel('searchWorker', channel); diff --git a/src/vs/workbench/services/search/node/legacy/worker/searchWorkerIpc.ts b/src/vs/workbench/services/search/node/legacy/worker/searchWorkerIpc.ts deleted file mode 100644 index 693574a3794..00000000000 --- a/src/vs/workbench/services/search/node/legacy/worker/searchWorkerIpc.ts +++ /dev/null @@ -1,64 +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 { IChannel, IServerChannel } from 'vs/base/parts/ipc/node/ipc'; -import { IPatternInfo, ITextSearchPreviewOptions } from 'vs/platform/search/common/search'; -import { SearchWorker } from './searchWorker'; -import { Event } from 'vs/base/common/event'; -import { ISerializedFileMatch } from 'vs/workbench/services/search/node/search'; - -export interface ISearchWorkerSearchArgs { - pattern: IPatternInfo; - fileEncoding: string; - absolutePaths: string[]; - maxResults?: number; - previewOptions?: ITextSearchPreviewOptions; -} - -export interface ISearchWorkerSearchResult { - matches: ISerializedFileMatch[]; - numMatches: number; - limitReached: boolean; -} - -export interface ISearchWorker { - initialize(): Promise; - search(args: ISearchWorkerSearchArgs): Promise; - cancel(): Promise; -} - -export class SearchWorkerChannel implements IServerChannel { - constructor(private worker: SearchWorker) { - } - - listen(): Event { - throw new Error('No events'); - } - - call(_, command: string, arg?: any): Promise { - switch (command) { - case 'initialize': return this.worker.initialize(); - case 'search': return this.worker.search(arg); - case 'cancel': return this.worker.cancel(); - } - throw new Error(`Call not found: ${command}`); - } -} - -export class SearchWorkerChannelClient implements ISearchWorker { - constructor(private channel: IChannel) { } - - initialize(): Promise { - return this.channel.call('initialize'); - } - - search(args: ISearchWorkerSearchArgs): Promise { - return this.channel.call('search', args); - } - - cancel(): Promise { - return this.channel.call('cancel'); - } -} diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index 7ed9c3da12f..da52da42622 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -19,7 +19,6 @@ import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'v import { MAX_FILE_SIZE } from 'vs/platform/files/node/files'; import { ICachedSearchStats, IFileQuery, IFileSearchStats, IFolderQuery, IProgress, IRawFileQuery, IRawQuery, IRawTextQuery, ITextQuery } from 'vs/platform/search/common/search'; import { Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch'; -import { LegacyTextSearchService } from 'vs/workbench/services/search/node/legacy/rawLegacyTextSearchService'; import { TextSearchEngineAdapter } from 'vs/workbench/services/search/node/textSearchAdapter'; import { IFileSearchProgressItem, IRawFileMatch, IRawSearchService, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess } from './search'; @@ -32,7 +31,6 @@ export class SearchService implements IRawSearchService { private static readonly BATCH_SIZE = 512; - private legacyTextSearchService = new LegacyTextSearchService(); private caches: { [cacheKey: string]: Cache; } = Object.create(null); fileSearch(config: IRawFileQuery): Event { @@ -64,9 +62,7 @@ export class SearchService implements IRawSearchService { const emitter = new Emitter({ onFirstListenerDidAdd: () => { promise = createCancelablePromise(token => { - return (rawQuery.useRipgrep ? - this.ripgrepTextSearch(query, p => emitter.fire(p), token) : - this.legacyTextSearchService.textSearch(query, p => emitter.fire(p), token)); + return this.ripgrepTextSearch(query, p => emitter.fire(p), token); }); promise.then( @@ -394,6 +390,9 @@ export class SearchService implements IRawSearchService { catch(reject?) { return this.then(undefined, reject); } + finally(onFinally) { + return promise.finally(onFinally); + } }; } } diff --git a/src/vs/workbench/services/search/node/searchService.ts b/src/vs/workbench/services/search/node/searchService.ts index 8020ff5fd02..8d45afec201 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -315,14 +315,12 @@ export class SearchService extends Disposable implements ISearchService { "type" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "endToEndTime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "sortingTime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "traversal" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "fileWalkTime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "directoriesWalked" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "filesWalked" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "cmdTime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "cmdResultCount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "scheme" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "useRipgrep" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + "scheme" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } } */ this.telemetryService.publicLog('searchComplete', { @@ -332,14 +330,12 @@ export class SearchService extends Disposable implements ISearchService { type: fileSearchStats.type, endToEndTime: endToEndTime, sortingTime: fileSearchStats.sortingTime, - traversal: searchEngineStats.traversal, fileWalkTime: searchEngineStats.fileWalkTime, directoriesWalked: searchEngineStats.directoriesWalked, filesWalked: searchEngineStats.filesWalked, cmdTime: searchEngineStats.cmdTime, cmdResultCount: searchEngineStats.cmdResultCount, - scheme, - useRipgrep: query.useRipgrep + scheme }); } } else if (query.type === QueryType.Text) { @@ -370,7 +366,6 @@ export class SearchService extends Disposable implements ISearchService { endToEndTime: endToEndTime, scheme, error: errorType, - useRipgrep: query.useRipgrep, usePCRE2: !!query.usePCRE2 }); } diff --git a/src/vs/workbench/services/search/test/node/rawSearchService.test.ts b/src/vs/workbench/services/search/test/node/rawSearchService.test.ts index f70c142c406..de7cf1d7964 100644 --- a/src/vs/workbench/services/search/test/node/rawSearchService.test.ts +++ b/src/vs/workbench/services/search/test/node/rawSearchService.test.ts @@ -25,7 +25,6 @@ const MULTIROOT_QUERIES: IFolderQuery[] = [ ]; const stats: ISearchEngineStats = { - traversal: 'node', fileWalkTime: 0, cmdTime: 1, directoriesWalked: 2, diff --git a/src/vs/workbench/services/search/test/node/search.test.ts b/src/vs/workbench/services/search/test/node/search.test.ts index c72a41992e6..ff89992c919 100644 --- a/src/vs/workbench/services/search/test/node/search.test.ts +++ b/src/vs/workbench/services/search/test/node/search.test.ts @@ -81,7 +81,6 @@ suite('FileSearchEngine', () => { type: QueryType.File, folderQueries: ROOT_FOLDER_QUERY, maxResults: 1, - useRipgrep: false }); let count = 0; @@ -147,7 +146,6 @@ suite('FileSearchEngine', () => { folderQueries: ROOT_FOLDER_QUERY, includePattern: { '**/file.txt': true }, exists: true, - useRipgrep: false }); let count = 0; @@ -170,7 +168,6 @@ suite('FileSearchEngine', () => { folderQueries: ROOT_FOLDER_QUERY, includePattern: { '**/nofile.txt': true }, exists: true, - useRipgrep: false }); let count = 0; @@ -256,7 +253,6 @@ suite('FileSearchEngine', () => { '*.txt': true, '*.js': true }, - useRipgrep: true }); let count = 0; @@ -281,7 +277,6 @@ suite('FileSearchEngine', () => { '*.txt': true, '*.js': true }, - useRipgrep: true }); let count = 0; @@ -777,7 +772,7 @@ suite('FileWalker', () => { excludePattern: { '**/something': true } }); const cmd1 = walker.spawnFindCmd(TEST_ROOT_FOLDER); - walker.readStdout(cmd1, 'utf8', /*isRipgrep=*/false, (err1, stdout1) => { + walker.readStdout(cmd1, 'utf8', (err1, stdout1) => { assert.equal(err1, null); assert.notStrictEqual(stdout1!.split('\n').indexOf(file0), -1, stdout1); assert.notStrictEqual(stdout1!.split('\n').indexOf(file1), -1, stdout1); @@ -788,7 +783,7 @@ suite('FileWalker', () => { excludePattern: { '**/subfolder': true } }); const cmd2 = walker.spawnFindCmd(TEST_ROOT_FOLDER); - walker.readStdout(cmd2, 'utf8', /*isRipgrep=*/false, (err2, stdout2) => { + walker.readStdout(cmd2, 'utf8', (err2, stdout2) => { assert.equal(err2, null); assert.notStrictEqual(stdout1!.split('\n').indexOf(file0), -1, stdout1); assert.strictEqual(stdout2!.split('\n').indexOf(file1), -1, stdout2); @@ -816,7 +811,7 @@ suite('FileWalker', () => { const walker = new FileWalker({ type: QueryType.File, folderQueries }); const cmd1 = walker.spawnFindCmd(folderQueries[0]); - walker.readStdout(cmd1, 'utf8', /*isRipgrep=*/false, (err1, stdout1) => { + walker.readStdout(cmd1, 'utf8', (err1, stdout1) => { assert.equal(err1, null); assert(outputContains(stdout1!, file0), stdout1); assert(!outputContains(stdout1!, file1), stdout1); @@ -837,7 +832,7 @@ suite('FileWalker', () => { const walker = new FileWalker({ type: QueryType.File, folderQueries: ROOT_FOLDER_QUERY, excludePattern: { '**/something': true } }); const cmd1 = walker.spawnFindCmd(TEST_ROOT_FOLDER); - walker.readStdout(cmd1, 'utf8', /*isRipgrep=*/false, (err1, stdout1) => { + walker.readStdout(cmd1, 'utf8', (err1, stdout1) => { assert.equal(err1, null); assert.notStrictEqual(stdout1!.split('\n').indexOf(file0), -1, stdout1); assert.notStrictEqual(stdout1!.split('\n').indexOf(file1), -1, stdout1); @@ -845,7 +840,7 @@ suite('FileWalker', () => { const walker = new FileWalker({ type: QueryType.File, folderQueries: ROOT_FOLDER_QUERY, excludePattern: { '{**/examples,**/more}': true } }); const cmd2 = walker.spawnFindCmd(TEST_ROOT_FOLDER); - walker.readStdout(cmd2, 'utf8', /*isRipgrep=*/false, (err2, stdout2) => { + walker.readStdout(cmd2, 'utf8', (err2, stdout2) => { assert.equal(err2, null); assert.notStrictEqual(stdout1!.split('\n').indexOf(file0), -1, stdout1); assert.strictEqual(stdout2!.split('\n').indexOf(file1), -1, stdout2); @@ -867,14 +862,14 @@ suite('FileWalker', () => { const walker = new FileWalker({ type: QueryType.File, folderQueries: ROOT_FOLDER_QUERY, excludePattern: { '**/examples/something': true } }); const cmd1 = walker.spawnFindCmd(TEST_ROOT_FOLDER); - walker.readStdout(cmd1, 'utf8', /*isRipgrep=*/false, (err1, stdout1) => { + walker.readStdout(cmd1, 'utf8', (err1, stdout1) => { assert.equal(err1, null); assert.notStrictEqual(stdout1!.split('\n').indexOf(file0), -1, stdout1); assert.notStrictEqual(stdout1!.split('\n').indexOf(file1), -1, stdout1); const walker = new FileWalker({ type: QueryType.File, folderQueries: ROOT_FOLDER_QUERY, excludePattern: { '**/examples/subfolder': true } }); const cmd2 = walker.spawnFindCmd(TEST_ROOT_FOLDER); - walker.readStdout(cmd2, 'utf8', /*isRipgrep=*/false, (err2, stdout2) => { + walker.readStdout(cmd2, 'utf8', (err2, stdout2) => { assert.equal(err2, null); assert.notStrictEqual(stdout1!.split('\n').indexOf(file0), -1, stdout1); assert.strictEqual(stdout2!.split('\n').indexOf(file1), -1, stdout2); @@ -895,14 +890,14 @@ suite('FileWalker', () => { const walker = new FileWalker({ type: QueryType.File, folderQueries: ROOT_FOLDER_QUERY, excludePattern: { '**/subfolder/something': true } }); const cmd1 = walker.spawnFindCmd(TEST_ROOT_FOLDER); - walker.readStdout(cmd1, 'utf8', /*isRipgrep=*/false, (err1, stdout1) => { + walker.readStdout(cmd1, 'utf8', (err1, stdout1) => { assert.equal(err1, null); assert.notStrictEqual(stdout1!.split('\n').indexOf(file0), -1, stdout1); assert.notStrictEqual(stdout1!.split('\n').indexOf(file1), -1, stdout1); const walker = new FileWalker({ type: QueryType.File, folderQueries: ROOT_FOLDER_QUERY, excludePattern: { '**/subfolder/anotherfolder': true } }); const cmd2 = walker.spawnFindCmd(TEST_ROOT_FOLDER); - walker.readStdout(cmd2, 'utf8', /*isRipgrep=*/false, (err2, stdout2) => { + walker.readStdout(cmd2, 'utf8', (err2, stdout2) => { assert.equal(err2, null); assert.notStrictEqual(stdout1!.split('\n').indexOf(file0), -1, stdout1); assert.strictEqual(stdout2!.split('\n').indexOf(file1), -1, stdout2); @@ -923,14 +918,14 @@ suite('FileWalker', () => { const walker = new FileWalker({ type: QueryType.File, folderQueries: ROOT_FOLDER_QUERY, excludePattern: { 'examples/something': true } }); const cmd1 = walker.spawnFindCmd(TEST_ROOT_FOLDER); - walker.readStdout(cmd1, 'utf8', /*isRipgrep=*/false, (err1, stdout1) => { + walker.readStdout(cmd1, 'utf8', (err1, stdout1) => { assert.equal(err1, null); assert.notStrictEqual(stdout1!.split('\n').indexOf(file0), -1, stdout1); assert.notStrictEqual(stdout1!.split('\n').indexOf(file1), -1, stdout1); const walker = new FileWalker({ type: QueryType.File, folderQueries: ROOT_FOLDER_QUERY, excludePattern: { 'examples/subfolder': true } }); const cmd2 = walker.spawnFindCmd(TEST_ROOT_FOLDER); - walker.readStdout(cmd2, 'utf8', /*isRipgrep=*/false, (err2, stdout2) => { + walker.readStdout(cmd2, 'utf8', (err2, stdout2) => { assert.equal(err2, null); assert.notStrictEqual(stdout1!.split('\n').indexOf(file0), -1, stdout1); assert.strictEqual(stdout2!.split('\n').indexOf(file1), -1, stdout2); @@ -967,7 +962,7 @@ suite('FileWalker', () => { } }); const cmd1 = walker.spawnFindCmd(TEST_ROOT_FOLDER); - walker.readStdout(cmd1, 'utf8', /*isRipgrep=*/false, (err1, stdout1) => { + walker.readStdout(cmd1, 'utf8', (err1, stdout1) => { assert.equal(err1, null); for (const fileIn of filesIn) { assert.notStrictEqual(stdout1!.split('\n').indexOf(fileIn), -1, stdout1); diff --git a/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts b/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts index 6d776117484..8b56afda721 100644 --- a/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts +++ b/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts @@ -9,15 +9,10 @@ import { getPathFromAmdModule } from 'vs/base/common/amd'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as glob from 'vs/base/common/glob'; import { URI } from 'vs/base/common/uri'; -import { IFolderQuery, ISearchRange, ITextQuery, ITextSearchMatch, QueryType, ITextSearchContext, deserializeSearchError, SearchErrorCode } from 'vs/platform/search/common/search'; -import { LegacyTextSearchService } from 'vs/workbench/services/search/node/legacy/rawLegacyTextSearchService'; +import { deserializeSearchError, IFolderQuery, ISearchRange, ITextQuery, ITextSearchContext, ITextSearchMatch, QueryType, SearchErrorCode } from 'vs/platform/search/common/search'; import { ISerializedFileMatch } from 'vs/workbench/services/search/node/search'; import { TextSearchEngineAdapter } from 'vs/workbench/services/search/node/textSearchAdapter'; -function countAll(matches: ISerializedFileMatch[]): number { - return matches.reduce((acc, m) => acc + m.numMatches!, 0); -} - const TEST_FIXTURES = path.normalize(getPathFromAmdModule(require, './fixtures')); const EXAMPLES_FIXTURES = path.join(TEST_FIXTURES, 'examples'); const MORE_FIXTURES = path.join(TEST_FIXTURES, 'more'); @@ -31,24 +26,7 @@ const MULTIROOT_QUERIES: IFolderQuery[] = [ { folder: URI.file(MORE_FIXTURES) } ]; -function doLegacySearchTest(config: ITextQuery, expectedResultCount: number | Function): Promise { - const engine = new LegacyTextSearchService(); - - let c = 0; - return engine.textSearch(config, (result) => { - if (result && Array.isArray(result)) { - c += countAll(result); - } - }, null!).then(() => { - if (typeof expectedResultCount === 'function') { - assert(expectedResultCount(c)); - } else { - assert.equal(c, expectedResultCount, 'legacy'); - } - }); -} - -function doRipgrepSearchTest(query: ITextQuery, expectedResultCount: number | Function): Promise { +function doSearchTest(query: ITextQuery, expectedResultCount: number | Function): Promise { const engine = new TextSearchEngineAdapter(query); let c = 0; @@ -69,11 +47,6 @@ function doRipgrepSearchTest(query: ITextQuery, expectedResultCount: number | Fu }); } -function doSearchTest(query: ITextQuery, expectedResultCount: number) { - return doLegacySearchTest(query, expectedResultCount) - .then(() => doRipgrepSearchTest(query, expectedResultCount)); -} - suite('Search-integration', function () { this.timeout(1000 * 60); // increase timeout for this suite @@ -240,10 +213,7 @@ suite('Search-integration', function () { maxResults }; - // (Legacy) search can go over the maxResults because it doesn't trim the results from its worker processes to the exact max size. - // But the worst-case scenario should be 2*max-1 - return doLegacySearchTest(config, count => count < maxResults * 2) - .then(() => doRipgrepSearchTest(config, maxResults)); + return doSearchTest(config, maxResults); }); test('Text: a (no results)', () => { @@ -318,7 +288,7 @@ suite('Search-integration', function () { contentPattern: { pattern: '语' } }; - return doRipgrepSearchTest(config, 1).then(results => { + return doSearchTest(config, 1).then(results => { const matchRange = (results[0].results![0]).ranges; assert.deepEqual(matchRange, [{ startLineNumber: 0, @@ -336,7 +306,7 @@ suite('Search-integration', function () { contentPattern: { pattern: 'h\\d,', isRegExp: true } }; - return doRipgrepSearchTest(config, 15).then(results => { + return doSearchTest(config, 15).then(results => { assert.equal(results.length, 3); assert.equal(results[0].results!.length, 1); const match = results[0].results![0]; @@ -353,7 +323,7 @@ suite('Search-integration', function () { afterContext: 2 }; - return doRipgrepSearchTest(config, 4).then(results => { + return doSearchTest(config, 4).then(results => { assert.equal(results.length, 4); assert.equal((results[0].results![0]).lineNumber, 25); assert.equal((results[0].results![0]).text, ' compiler.addUnit(prog,"input.ts");'); @@ -378,7 +348,7 @@ suite('Search-integration', function () { contentPattern: { pattern: 'test' }, }; - return doRipgrepSearchTest(config, 0).then(() => { + return doSearchTest(config, 0).then(() => { throw new Error('expected fail'); }, err => { const searchError = deserializeSearchError(err.message); @@ -394,7 +364,7 @@ suite('Search-integration', function () { contentPattern: { pattern: ')', isRegExp: true }, }; - return doRipgrepSearchTest(config, 0).then(() => { + return doSearchTest(config, 0).then(() => { throw new Error('expected fail'); }, err => { const searchError = deserializeSearchError(err.message); @@ -413,7 +383,7 @@ suite('Search-integration', function () { } }; - return doRipgrepSearchTest(config, 0).then(() => { + return doSearchTest(config, 0).then(() => { throw new Error('expected fail'); }, err => { const searchError = deserializeSearchError(err.message); @@ -429,7 +399,7 @@ suite('Search-integration', function () { contentPattern: { pattern: 'foo\nbar', isRegExp: true } }; - return doRipgrepSearchTest(config, 0).then(() => { + return doSearchTest(config, 0).then(() => { throw new Error('expected fail'); }, err => { const searchError = deserializeSearchError(err.message); diff --git a/src/vs/workbench/services/workspace/common/workspaceEditing.ts b/src/vs/workbench/services/workspace/common/workspaceEditing.ts index db73788241f..e65b8e1c29e 100644 --- a/src/vs/workbench/services/workspace/common/workspaceEditing.ts +++ b/src/vs/workbench/services/workspace/common/workspaceEditing.ts @@ -34,18 +34,18 @@ export interface IWorkspaceEditingService { /** * enters the workspace with the provided path. */ - enterWorkspace(path: string): Promise; + enterWorkspace(path: URI): Promise; /** * creates a new workspace with the provided folders and opens it. if path is provided * the workspace will be saved into that location. */ - createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): Promise; + createAndEnterWorkspace(folders: IWorkspaceFolderCreationData[], path?: URI): Promise; /** - * saves the workspace to the provided path and opens it. requires a workspace to be opened. + * saves the current workspace to the provided path and opens it. requires a workspace to be opened. */ - saveAndEnterWorkspace(path: string): Promise; + saveAndEnterWorkspace(path: URI): Promise; /** * copies current workspace settings to the target workspace. diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index 38bc550917e..6172c7e23fb 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -7,9 +7,9 @@ import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IWindowService, IEnterWorkspaceResult } from 'vs/platform/windows/common/windows'; +import { IWindowService, MessageBoxOptions, IWindowsService } from 'vs/platform/windows/common/windows'; import { IJSONEditingService, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing'; -import { IWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, isWorkspaceIdentifier, toWorkspaceIdentifier, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -22,8 +22,10 @@ import { BackupFileService } from 'vs/workbench/services/backup/node/backupFileS import { ICommandService } from 'vs/platform/commands/common/commands'; import { distinct } from 'vs/base/common/arrays'; import { isLinux } from 'vs/base/common/platform'; -import { isEqual } from 'vs/base/common/resources'; +import { isEqual, basename } from 'vs/base/common/resources'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IFileService } from 'vs/platform/files/common/files'; +import { rewriteWorkspaceFileForNewLocation } from 'vs/platform/workspaces/node/workspaces'; export class WorkspaceEditingService implements IWorkspaceEditingService { @@ -38,7 +40,10 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { @IExtensionService private readonly extensionService: IExtensionService, @IBackupFileService private readonly backupFileService: IBackupFileService, @INotificationService private readonly notificationService: INotificationService, - @ICommandService private readonly commandService: ICommandService + @ICommandService private readonly commandService: ICommandService, + @IFileService private readonly fileSystemService: IFileService, + @IWindowsService private readonly windowsService: IWindowsService, + @IWorkspacesService private readonly workspaceService: IWorkspacesService ) { } @@ -140,16 +145,64 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { return false; } - enterWorkspace(path: string): Promise { - return this.doEnterWorkspace(() => this.windowService.enterWorkspace(path)); + async createAndEnterWorkspace(folders: IWorkspaceFolderCreationData[], path?: URI): Promise { + if (path && !this.isValidTargetWorkspacePath(path)) { + return Promise.reject(null); + } + + const untitledWorkspace = await this.workspaceService.createUntitledWorkspace(folders); + if (path) { + await this.saveWorkspaceAs(untitledWorkspace, path); + } else { + path = URI.file(untitledWorkspace.configPath); + } + return this.enterWorkspace(path); } - createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): Promise { - return this.doEnterWorkspace(() => this.windowService.createAndEnterWorkspace(folders, path)); + async saveAndEnterWorkspace(path: URI): Promise { + if (!this.isValidTargetWorkspacePath(path)) { + return Promise.reject(null); + } + const currentWorkspaceIdentifier = toWorkspaceIdentifier(this.contextService.getWorkspace()); + if (!isWorkspaceIdentifier(currentWorkspaceIdentifier)) { + return Promise.reject(null); + } + await this.saveWorkspaceAs(currentWorkspaceIdentifier, path); + + return this.enterWorkspace(path); } - saveAndEnterWorkspace(path: string): Promise { - return this.doEnterWorkspace(() => this.windowService.saveAndEnterWorkspace(path)); + async isValidTargetWorkspacePath(path: URI): Promise { + + const windows = await this.windowsService.getWindows(); + + // Prevent overwriting a workspace that is currently opened in another window + if (windows.some(window => window.workspace && isEqual(URI.file(window.workspace.configPath), path))) { + const options: MessageBoxOptions = { + type: 'info', + buttons: [nls.localize('ok', "OK")], + message: nls.localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)), + detail: nls.localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), + noLink: true + }; + return this.windowService.showMessageBox(options).then(() => false); + } + + return Promise.resolve(true); // OK + } + + private async saveWorkspaceAs(workspace: IWorkspaceIdentifier, targetConfigPathURI: URI): Promise { + const configPathURI = URI.file(workspace.configPath); + + // Return early if target is same as source + if (isEqual(configPathURI, targetConfigPathURI)) { + return Promise.resolve(null); + } + + // Read the contents of the workspace file, update it to new location and save it. + const raw = await this.fileSystemService.resolveContent(configPathURI); + const newRawWorkspaceContents = rewriteWorkspaceFileForNewLocation(raw.value, configPathURI, targetConfigPathURI); + await this.fileSystemService.createFile(targetConfigPathURI, newRawWorkspaceContents, { overwrite: true }); } private handleWorkspaceConfigurationEditingError(error: JSONEditingError): Promise { @@ -185,7 +238,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { ); } - private doEnterWorkspace(mainSidePromise: () => Promise): Promise { + enterWorkspace(path: URI): Promise { // Stop the extension host first to give extensions most time to shutdown this.extensionService.stopExtensionHost(); @@ -196,7 +249,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { extensionHostStarted = true; }; - return mainSidePromise().then(result => { + return this.windowService.enterWorkspace(path).then(result => { // Migrate storage and settings if we are to enter a workspace if (result) { diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 354243890f3..3de18af03d2 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -44,7 +44,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IThemeService } from 'vs/platform/theme/common/themeService'; import { generateUuid } from 'vs/base/common/uuid'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IRecentlyOpened } from 'vs/platform/history/common/history'; import { ITextResourceConfigurationService, ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration'; import { IPosition, Position as EditorPosition } from 'vs/editor/common/core/position'; @@ -132,6 +132,10 @@ export class TestContextService implements IWorkspaceContextService { return WorkbenchState.EMPTY; } + getCompleteWorkspace(): Promise { + return Promise.resolve(this.getWorkspace()); + } + public getWorkspace(): IWorkbenchWorkspace { return this.workspace; } @@ -488,6 +492,8 @@ export class TestPartService implements IPartService { return false; } + public setEditorHidden(_hidden: boolean): Promise { return Promise.resolve(null); } + public setSideBarHidden(_hidden: boolean): Promise { return Promise.resolve(null); } public isPanelHidden(): boolean { @@ -1054,15 +1060,7 @@ export class TestWindowService implements IWindowService { return Promise.resolve(); } - enterWorkspace(_path: string): Promise { - return Promise.resolve(); - } - - createAndEnterWorkspace(_folders?: IWorkspaceFolderCreationData[], _path?: string): Promise { - return Promise.resolve(); - } - - saveAndEnterWorkspace(_path: string): Promise { + enterWorkspace(_path: URI): Promise { return Promise.resolve(); } @@ -1223,15 +1221,7 @@ export class TestWindowsService implements IWindowsService { return Promise.resolve(); } - enterWorkspace(_windowId: number, _path: string): Promise { - return Promise.resolve(); - } - - createAndEnterWorkspace(_windowId: number, _folders?: IWorkspaceFolderCreationData[], _path?: string): Promise { - return Promise.resolve(); - } - - saveAndEnterWorkspace(_windowId: number, _path: string): Promise { + enterWorkspace(_windowId: number, _path: URI): Promise { return Promise.resolve(); } diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 1cb536f7767..1ad274696dc 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -3,138 +3,176 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// Base -import 'vs/base/common/strings'; -import 'vs/base/common/errors'; +//#region --- workbench/editor core -// Configuration -import 'vs/workbench/services/configuration/common/configurationExtensionPoint'; - -// Editor import 'vs/editor/editor.all'; -// Platform -import 'vs/platform/widget/browser/contextScopedHistoryWidget'; +import 'vs/workbench/api/electron-browser/extensionHost.contribution'; -// Menus/Actions -import 'vs/workbench/services/actions/electron-browser/menusExtensionPoint'; +import 'vs/workbench/electron-browser/shell.contribution'; +import 'vs/workbench/browser/workbench.contribution'; -// Views +import 'vs/workbench/electron-browser/main'; + +//#endregion + + +//#region --- workbench actions + +import 'vs/workbench/browser/actions/layoutActions'; +import 'vs/workbench/browser/actions/listCommands'; +import 'vs/workbench/browser/actions/navigationActions'; +import 'vs/workbench/browser/parts/quickopen/quickopenActions'; +import 'vs/workbench/browser/parts/quickinput/quickInputActions'; + +//#endregion + + +//#region --- API Extension Points + +import 'vs/workbench/api/common/menusExtensionPoint'; +import 'vs/workbench/api/common/configurationExtensionPoint'; import 'vs/workbench/api/browser/viewsExtensionPoint'; +//#endregion + + +//#region --- workbench services + +import 'vs/workbench/services/bulkEdit/electron-browser/bulkEditService'; + +//#endregion + + +//#region --- workbench parts + // Localizations import 'vs/workbench/parts/localizations/electron-browser/localizations.contribution'; -// Workbench -import 'vs/workbench/browser/actions/toggleActivityBarVisibility'; -import 'vs/workbench/browser/actions/toggleStatusbarVisibility'; -import 'vs/workbench/browser/actions/toggleSidebarVisibility'; -import 'vs/workbench/browser/actions/toggleSidebarPosition'; -import 'vs/workbench/browser/actions/toggleEditorLayout'; -import 'vs/workbench/browser/actions/toggleZenMode'; -import 'vs/workbench/browser/actions/toggleCenteredLayout'; -import 'vs/workbench/browser/actions/toggleTabsVisibility'; +// Preferences import 'vs/workbench/parts/preferences/electron-browser/preferences.contribution'; import 'vs/workbench/parts/preferences/browser/keybindingsEditorContribution'; + +// Logs import 'vs/workbench/parts/logs/electron-browser/logs.contribution'; -import 'vs/workbench/browser/parts/quickopen/quickopen.contribution'; +// Quick Open Handlers import 'vs/workbench/parts/quickopen/browser/quickopen.contribution'; -import 'vs/workbench/browser/parts/editor/editorPicker'; -import 'vs/workbench/browser/parts/quickinput/quickInput.contribution'; +// Explorer import 'vs/workbench/parts/files/electron-browser/explorerViewlet'; import 'vs/workbench/parts/files/electron-browser/fileActions.contribution'; import 'vs/workbench/parts/files/electron-browser/files.contribution'; +// Backup import 'vs/workbench/parts/backup/common/backup.contribution'; +// Stats import 'vs/workbench/parts/stats/node/stats.contribution'; +// Rapid Render Splash import 'vs/workbench/parts/splash/electron-browser/partsSplash.contribution'; +// Search import 'vs/workbench/parts/search/electron-browser/search.contribution'; import 'vs/workbench/parts/search/browser/searchView'; import 'vs/workbench/parts/search/browser/openAnythingHandler'; +// SCM import 'vs/workbench/parts/scm/electron-browser/scm.contribution'; import 'vs/workbench/parts/scm/electron-browser/scmViewlet'; +// Debug import 'vs/workbench/parts/debug/electron-browser/debug.contribution'; import 'vs/workbench/parts/debug/browser/debugQuickOpen'; import 'vs/workbench/parts/debug/electron-browser/repl'; import 'vs/workbench/parts/debug/browser/debugViewlet'; +// Markers import 'vs/workbench/parts/markers/electron-browser/markers.contribution'; + +// Comments import 'vs/workbench/parts/comments/electron-browser/comments.contribution'; +// HTML Preview import 'vs/workbench/parts/html/electron-browser/html.contribution'; +// URL Support import 'vs/workbench/parts/url/electron-browser/url.contribution'; + +// Webview import 'vs/workbench/parts/webview/electron-browser/webview.contribution'; -import 'vs/workbench/parts/welcome/walkThrough/electron-browser/walkThrough.contribution'; - +// Extensions Management import 'vs/workbench/parts/extensions/electron-browser/extensions.contribution'; import 'vs/workbench/parts/extensions/browser/extensionsQuickOpen'; import 'vs/workbench/parts/extensions/electron-browser/extensionsViewlet'; -import 'vs/workbench/parts/welcome/page/electron-browser/welcomePage.contribution'; - +// Output Panel import 'vs/workbench/parts/output/electron-browser/output.contribution'; import 'vs/workbench/parts/output/browser/outputPanel'; +// Terminal import 'vs/workbench/parts/terminal/electron-browser/terminal.contribution'; import 'vs/workbench/parts/terminal/browser/terminalQuickOpen'; import 'vs/workbench/parts/terminal/electron-browser/terminalPanel'; -import 'vs/workbench/electron-browser/workbench'; - +// Relauncher import 'vs/workbench/parts/relauncher/electron-browser/relauncher.contribution'; +// Tasks import 'vs/workbench/parts/tasks/electron-browser/task.contribution'; +// Emmet import 'vs/workbench/parts/emmet/browser/emmet.browser.contribution'; import 'vs/workbench/parts/emmet/electron-browser/emmet.contribution'; -import 'vs/workbench/parts/codeEditor/codeEditor.contribution'; +// CodeEditor Contributions +import 'vs/workbench/parts/codeEditor/electron-browser/codeEditor.contribution'; +// Execution import 'vs/workbench/parts/execution/electron-browser/execution.contribution'; +// Snippets import 'vs/workbench/parts/snippets/electron-browser/snippets.contribution'; import 'vs/workbench/parts/snippets/electron-browser/snippetsService'; import 'vs/workbench/parts/snippets/electron-browser/insertSnippet'; import 'vs/workbench/parts/snippets/electron-browser/configureSnippets'; import 'vs/workbench/parts/snippets/electron-browser/tabCompletion'; -import 'vs/workbench/parts/themes/electron-browser/themes.contribution'; - +// Send a Smile import 'vs/workbench/parts/feedback/electron-browser/feedback.contribution'; -import 'vs/workbench/parts/welcome/gettingStarted/electron-browser/gettingStarted.contribution'; - +// Update import 'vs/workbench/parts/update/electron-browser/update.contribution'; +// Surveys import 'vs/workbench/parts/surveys/electron-browser/nps.contribution'; import 'vs/workbench/parts/surveys/electron-browser/languageSurveys.contribution'; +// Performance import 'vs/workbench/parts/performance/electron-browser/performance.contribution'; +// CLI import 'vs/workbench/parts/cli/electron-browser/cli.contribution'; -import 'vs/workbench/api/electron-browser/extensionHost.contribution'; - -import 'vs/workbench/electron-browser/main.contribution'; -import 'vs/workbench/electron-browser/main'; - +// Themes Support +import 'vs/workbench/parts/themes/electron-browser/themes.contribution'; import 'vs/workbench/parts/themes/test/electron-browser/themes.test.contribution'; +// Watermark import 'vs/workbench/parts/watermark/electron-browser/watermark'; +// Welcome +import 'vs/workbench/parts/welcome/walkThrough/electron-browser/walkThrough.contribution'; +import 'vs/workbench/parts/welcome/gettingStarted/electron-browser/gettingStarted.contribution'; import 'vs/workbench/parts/welcome/overlay/browser/welcomeOverlay'; +import 'vs/workbench/parts/welcome/page/electron-browser/welcomePage.contribution'; +// Outline import 'vs/workbench/parts/outline/electron-browser/outline.contribution'; -import 'vs/workbench/services/bulkEdit/electron-browser/bulkEditService'; - +// Experiments import 'vs/workbench/parts/experiments/electron-browser/experiments.contribution'; + +//#endregion \ No newline at end of file diff --git a/test/smoke/package.json b/test/smoke/package.json index f3c439c99a8..fe5adc19357 100644 --- a/test/smoke/package.json +++ b/test/smoke/package.json @@ -22,7 +22,7 @@ "@types/webdriverio": "4.6.1", "concurrently": "^3.5.1", "cpx": "^1.5.0", - "electron": "3.1.0", + "electron": "3.1.2", "htmlparser2": "^3.9.2", "mkdirp": "^0.5.1", "mocha": "^5.2.0", diff --git a/test/smoke/yarn.lock b/test/smoke/yarn.lock index a44a612cf72..6ca02b80baf 100644 --- a/test/smoke/yarn.lock +++ b/test/smoke/yarn.lock @@ -596,10 +596,10 @@ electron-download@^4.1.0: semver "^5.4.1" sumchecker "^2.0.2" -electron@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-3.1.0.tgz#5e36ba4c24926c7cf80eccf6f8361b3cad409f17" - integrity sha512-FnHH3T7aQGAjw5h8//9BNLZBByP/gnEGP3sQH5if7HVe6Znz5KcsRbIdxLYVH9DXJFoJ2SArP+UiAAYQIdVQJQ== +electron@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/electron/-/electron-3.1.2.tgz#e410b976c56fc2f783c3b0fb6d757e02eaeab902" + integrity sha512-B/mXRCN8jGBBx8dvtIgLyW+nE8i9y7K9G6wijU+cLoneqF5al9BgZA1l5xgZEiUrwTtt0cgXIWNwhStt7EDoQQ== dependencies: "@types/node" "^8.0.24" electron-download "^4.1.0" diff --git a/tslint.json b/tslint.json index 919f55305f5..67a640687c9 100644 --- a/tslint.json +++ b/tslint.json @@ -393,21 +393,6 @@ "*" // node modules ] }, - { - "target": "**/vs/workbench/node/**", - "restrictions": [ - "vs/nls", - "**/vs/base/{common,node}/**", - "**/vs/base/parts/*/{common,node}/**", - "**/vs/platform/node/**", - "**/vs/platform/*/{common,node}/**", - "**/vs/editor/{common,node}/**", - "**/vs/editor/contrib/*/{common,node}/**", - "**/vs/workbench/{common,node,api}/**", - "**/vs/workbench/services/*/{common,node}/**", - "*" // node modules - ] - }, { "target": "**/vs/workbench/services/**/test/**", "restrictions": [ @@ -454,6 +439,7 @@ "**/vs/platform/**/{common,node}/**", "**/vs/editor/{common,node}/**", "**/vs/workbench/{common,node}/**", + "**/vs/workbench/api/{common,node}/**", "**/vs/workbench/services/**/{common,node}/**", "*" // node modules ] diff --git a/yarn.lock b/yarn.lock index 9dbb3c2151d..82332aa7d79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9399,10 +9399,10 @@ vscode-nsfw@1.1.1: lodash.isundefined "^3.0.1" nan "^2.10.0" -vscode-proxy-agent@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/vscode-proxy-agent/-/vscode-proxy-agent-0.2.0.tgz#5ae6e1f8e1b8715a4058fafb6d10cd2409d72620" - integrity sha512-x6RUOUkV18iM78+6S70VjiwiHuY7qmk3CU9+u+0y4c1Y/X8/IcTJfPsngoAXrrojoKcplyLdp3YCZbj1rFNo/Q== +vscode-proxy-agent@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/vscode-proxy-agent/-/vscode-proxy-agent-0.3.0.tgz#b5c8bea5046761966e1fa71f89d9cef11c457894" + integrity sha512-R6qz8Sc0ocNfeFPOp3k6QLP/Y8HzK1yqXwfgB1f0GakVzUGMDmniRe8RLxIiCAqlxGaWMn2yqpTSNUYZ1obPsQ== dependencies: debug "3.1.0" http-proxy-agent "2.1.0" @@ -9433,10 +9433,10 @@ vscode-uri@^1.0.6: resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.6.tgz#6b8f141b0bbc44ad7b07e94f82f168ac7608ad4d" integrity sha512-sLI2L0uGov3wKVb9EB+vIQBl9tVP90nqRvxSoJ35vI3NjxE8jfsE5DSOhWgSunHSZmKS4OCi2jrtfxK7uyp2ww== -vscode-xterm@3.10.0-beta11-reflow: - version "3.10.0-beta11-reflow" - resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.10.0-beta11-reflow.tgz#e92eb61163489957ba2d4ec978ddebd0bb7b6631" - integrity sha512-BrOMsRAgsZ/lDTtgsRdlHPAFy4ZDr6MxVGYbQzysO4RTP010kno5qmOvctQP/6tRDl9gymUHZIy1FmjfsmdxcQ== +vscode-xterm@3.11.0-beta1: + version "3.11.0-beta1" + resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.11.0-beta1.tgz#1c4dac56e8e87839e8cbc4a6481916f578e3ad8e" + integrity sha512-9T1uswvZ4fGGa+rXCT7AFmTbwQsuARoyG75FzTr0MYDXxg0hufnofWZ73LBn92yY/GP9bv2oJZ5Wc4LDlX6OJQ== vso-node-api@6.1.2-preview: version "6.1.2-preview"